纯Socket(BIO)长链接编程的常见的坑和填坑套路
Internet(全球互联网)是无数台机器基于TCP/IP协议族相互通信产生的。TCP/IP协议族分了四层实现,链路层、网络层、传输层、应用层。
真正正文
|
顺序
|
字段名 | 长度(字节) |
字段类型
|
描述
|
|
1
|
消息长度 |
4(32bit)
|
int | socket报文的长度最长2^31-1字节,大文件传输不使用此字段 |
|
2
|
行为标识
|
1(8bit)
|
byte
|
用于分支处理数据1字节可标识256种行为,一般够用
|
|
3
|
加密标识 |
1(8bit)
|
byte
|
区分加密方式0不加密
|
|
4
|
时间戳 |
8(64bit)
|
long | 消息时间戳,其实也没啥用,加着玩的,忽视掉吧 |
|
5
|
消息体
|
|
String
|
长度为消息长度-10字节,建议使用json,具体解析行为由行为标识字段定义
|
2、规定通信工作流程
关于性能方面可以 使用队列+线程池+连接池相互配合,这次先不讨论这些,想要讨论的可以私信我或评论,一起讨论。
/** 消息包(报文) **/
class SocketPackage {
int length;// 长度
byte action;// 行为标识
byte encryption;// 加密标识
long timestamp;// 时间戳
String data;// 消息体 /** TODO:将此消息包转换为适当的byte数组 **/
byte[] toBytes() { byte[] lengthBytes = int2bytes(length);
// ...将各个字段都做了转换成bytes的操作后,合并byte数组并返回
} /** TODO:读取输入流转换成一个消息包 **/
static SocketPackage parse(InputStream in) throws IOException {
SocketPackage sp = new SocketPackage();
byte[] lengthBytes = new byte[4];
in.read(lengthBytes);// 未收到信息时此步将会阻塞
sp.length = bytes2int(lengthBytes);
// .....其他字段读取就不写了,这里要控制好异常,不要随意catch住,如果发生异常,不是socket坏了就是报文异常了,应当采用拒绝连接的形式向对方跑出异常
} }
/** 封装下socket,使其可以保存更多的连接信息,不要纠结名字,我纠结了好一会儿不知道怎么命名,反正是伪代码,就这样写着吧 **/
class NiuxzSocket {
Socket socket;
volatile long lastUse;// 上次使用时间
// ...这里还可以再加其他属性,比如是否是写状态,写操作开始时间,上次非心跳包时间等 NiuxzSocket(Socket socket) {
this.socket = socket;
this.lastUse = System.currentTimeMillis();
} InputStream getIn() {
return socket.getInputStream();
} void write(byte[] bytes) throws IOException {
this.socket.getOutputStream().write(bytes);
}
}
/** 封装一个发送信息的接口,提供常用的发送信息方法。 **/
interface SocketClient {
SocketPackage sendData(SocketPackage sp);// 发送一个消息包,并等待返回的消息包
// TODO:还可以根据双方的业务和协议添加几个更方便使用的接口方法。比如只返回消息体字段,或者直接返回json内容的 void sendHeartBeat(NiuxzSocket socket);// 发送一个心跳包,这个方法后面讲心跳包时会用到
} class DefaultSocketClient implements SocketClient {
SocketPool socketPool;// 先假装有一个socket连接池,用来管理socket。不使用连接池的话,在这里直接注入一个NiuxzSocket就可以了。下面代码中也直接使用socket,但是一定要在使用时进行加锁操作。否则就会造成多线程访问同一个socket导致数据错乱了。 /** 此方法就是主动端工作入口了,业务代码可以直接调用这里进行发送数据 **/
SocketPackage sendData(SocketPackage sp){
NiuxzSocket niuxzSocket = socketPool.get();//获取一个socket,这里可以看到获取的socket并不是原生的socket,其实是我们自己封装后的socket
try{
niuxzSocket.write(sp.toBytes());//阻塞持续写到缓存中
niuxzSocket.lastUse = System.currentTimeMillis();//根据业务方法更新socket的状态信息
SocketPackage sp = SocketPackage.parse(niuxzSocket.getIn());//阻塞读,等待消息的返回,因为是单线程操作socket所以不存在消息插队的情况。
return sp;
}catch(Exception e){
LOG.error("发送消息包失败",e);
socketPool.destroy(niuxzSocket)
//在发生不可复用的异常时才关闭socket,并销毁这个NiuxzSocke。不可复用异常意思是IO操作到了一半不知道具体到哪了所以整个socket都不可用了。
}
finally{
if(socketPool!=null){
socketPool.recycle(niuxzSocket );//使用完这个socket后我们不要关闭,因为还要复用,让连接池回收这个socket。recycle内要判断socket是否是销毁状态。
}
}
}
}
/** 定义一个连接池接口SocketPool **/
interface SocketPool {
/** 获取一个连接 **/
NiuxzSocket get(); /** 回收Socket **/
void recycle(NiuxzSocket ns); /** 销毁Socket **/
void destroy(NiuxzSocket ns);
} /** 实现连接池 **/
class DefaultSocketPool implements SocketPool {
BlockingQueue<NiuxzSocket> sockets;// 存放socket的容器,也可以使用数组 NiuxzSocket get() {
// TODO:池里有就获取,没有就开一个线程去创建 并且等待创建完成,可使用synchronized/wait或Lock/condition
}
// TODO:实现socketPool,实现连接池是属于性能可靠性优化,要做的事情会比较多。偷个懒,大家懂就好,具体实现,等有时间我把我的连接池代码整理后再写一篇文章,有想了解的可以给我评论讨论下。
}
/**开启一个ServerSocket并等待连接,联入后开启一个线程进行处理**/
class NiuxzServer{
ServerSocket serverSocket;
HashMap<NiuxzSocket> sockets = new HashMap<NiuxzSocket>();
public static AtomicInteger workerCount = 0;
public Object waitLock = new Object();
int maxWorkerCount = 100;//允许100个连接进入
int port;//配置一个端口号 /**工作入口**/
void work(){
serverSocket = new ServerSocket(port);
while(true){
Socket socekt = serverSocket.accept();//阻塞等待连接
NiuxzSocket niuxzSocket = new NiuxzSocket(socket);
sockets.put(niuxzSocket ,1);//将连接放入map中
Worker worker = new Worker(niuxzSocket );//创建一个工作线程
worker.start();//开始线程
while(true){
if(workerCount.incrementAndGet()>=maxWorkerCount){//如果超过了规定的最大线程数,就进入等待,等待其他连接销毁
synchronized(waitLock){
if(workerCount.incrementAndGet()>=maxWorkerCount){//double check 确定进入等待前没有正在断开的socket
waitLock.wait();
}else{
break;
}
}
}else{
break;
}
}
}
} /**销毁一个连接**/
void destroy(NiuxzSocket socket){
synchronized(waitLock){
sockets.remove(socket);//从池子里删除
workerCount.decrementAndGet();//当前连接数减一
waitLock.notify();//通知work方法 可以继续接受请求了
}
} /**创建一个工作者线程类,处理连入的socket**/
class Worker extends Thread{
HashMap<Integer,SocketHandler> handlers;//针对每种行为标识做的消息处理器。
NiuxzSocket socket;
Worker(NiuxzSocketsocket){//构造函数
this.socket = socket;
}
void run(){
try{
while(true){
SocketPackage sp = SocketPackage.parse(socket.getIn());//阻塞读,直到读完一个消息包未知,这样可以解决粘包或半包的问题
SocketHandler handler = handlers.get(sp.getAction());//根据行为标识获取响应的处理器
handler.handle(sp,socket);//处理结果和响应信息都在handler中回写
}
}cache(Exception e){
LOG.error("连接异常中断",e);
NiuxzServer.destroy(socket);
}
}
}
}
/** 创建一个消息处理器 SocketHandler 接收所有内容后 回显 **/
class EchoSocketHandler implements SocketHandler {
/** 处理socket请求 **/
void handle(SocketPackage sp, NiuxzSocket socket) {
sp.setAction(10);// 比如协议中的行为标识10是响应成功的意思
socket.write(sp.toBytes());// 直接回写
}
}
至此两端的工作代码已经初步完成。socket可以按照相互制定的通讯方式进行通讯了。
3、心跳机制:
心跳机制socket长链接通讯中不可或缺的一个机制。主动端可以检测socket是否存活,被动端可以检测对方是否还在线。因为有时候网络并不一定那么完美,会出现链路上的异常,此时应用层可能并不能发现问题,等下次再用这个连接的时候就会抛出异常了,如果是被动端,还会白白占用着一个线程,不如在那之前就发现一部分异常,并销毁连接,下次通讯时出错的概率就降低了很多,被动端也会释放线程,释放资源。
@Scheduled(fixedDelay=30*1000)//延时30秒执行一次
void HeartBeat(){
for(NiuxzSocket socket:socketPool.getAllSocket()){
if(System.curTime() - socket.getLastUse() > 30*1000){//如果系统时间减上次使用时间大于30秒
//开启线程,从连接池中取出这个连接remove(socket)移除成功再继续操作,保证不会有其他线程同时使用这个socket。发送一个SocketPackage,socketClient.sendHeartBeat()
if(socketPool.remove(socket)){
socketClient.snedHeartBeat(socket);//socketClient.snedHeartBeat这个方法实现:行为标识设置为心跳包,比如规定1就是心跳包。完事回收这个链接socketPool.recycle(socket),但当中间反生异常,则代表这个连接不可用了,就销毁socketPool.destroy(socket)。
}
}
}
}
被动端:
以上便是我用同步socket实现第一版分布式文件系统时总结的经验,有些问题其实在NIO中变得不是问题了。NIO和AIO更适合会持有大量连接的服务器端。
纯Socket(BIO)长链接编程的常见的坑和填坑套路的更多相关文章
- socket套接字编程 HTTP协议
socket套接字编程 套接字介绍 1. 套接字 : 实现网络编程进行数据传输的一种技术手段 2. Python实现套接字编程:import socket 3. 套接字分类 >流式套接 ...
- uwsgi支持http长链接
http1.1支持长链接,而http1.0不支持,所以,在切换http版本号或者升级服务端版本时候,尤其要注意这个造成的影响. 当客户端以http1.1长链接方式连接服务端时,服务端如果不支持1.1, ...
- socket 套接字编程
今日内容 socket 套接字编程 简易服务端与客户端代码实现 通信循环 黏包现象(TCP协议) 报头制作.struct 模块.封装形式 内容详细 一.socket 套接字编程 实现一款能够进行数据交 ...
- python+uwsgi导致redis无法长链接引起性能下降问题记录
今天在部署python代码到预生产环境时,web站老是出现redis链接未初始化,无法连接到服务的提示,比对了一下开发环境与测试环境代码,完全一致,然后就是查看各种日志,排查了半天也没有查明是什么原因 ...
- PHP实现新浪长链接转化成短链接API
我们经常收到类似于这样的短信(如下图),发现其中的链接并不是常规的网址链接,而是个短小精悍的短链接,产品中经常需要这样的需求,如果在给用户下发的短信中是一个很长的连接,用户体验肯定很差,因此我们需要实 ...
- 长链接转换成短链接(iOS版本)
首先需要将字符串使用md5加密,添加NSString的md5的类别方法如下 .h文件 #import <CommonCrypto/CommonDigest.h> @interface NS ...
- linux网络环境下socket套接字编程(UDP文件传输)
今天我们来介绍一下在linux网络环境下使用socket套接字实现两个进程下文件的上传,下载,和退出操作! 在socket套接字编程中,我们当然可以基于TCP的传输协议来进行传输,但是在文件的传输中, ...
- linux网络编程-(socket套接字编程UDP传输)
今天我们来介绍一下在linux网络环境下使用socket套接字实现两个进程下文件的上传,下载,和退出操作! 在socket套接字编程中,我们当然可以基于TCP的传输协议来进行传输,但是在文件的传输中, ...
- Java多线程编程的常见陷阱(转)
Java多线程编程的常见陷阱 2009-06-16 13:48 killme2008 blogjava 字号:T | T 本文介绍了Java多线程编程中的常见陷阱,如在构造函数中启动线程,不完全的同步 ...
随机推荐
- 使用getCurrentPosition方法实时获取当前Geolocation信息(赋源码文件)--html5、JavaScript
使用getCurrentPosition方法实时获取当前Geolocation信息: 1.typeof 运算符返回一个用来表示表达式的数据类型的字符串 <script> alert(typ ...
- uva11584
将课本上所述方法实现即可,代码如下: /* * Author: Bingo * Created Time: 2015/1/25 23:49:49 * File Name: uva11584.cpp * ...
- js页面间通信方法(storage事件)(浏览器页面间通信方法)
在写页面的时候有时会遇到这样的需求,需要两个页面之间传递数据或者一个事件.这个时候,就需要用到我今天所要讲的storage事件,学习这个事件之前,需要先了解localStorage的用法.具体用法可以 ...
- 【UML 建模】活动图介绍
1.活动图,即Activity Diagram,是UML中用于对系统的动态行为建模的一种常用工具,它描述活动的顺序,展现从一种活动到另一种活动的控制流.其本质上是一种流程图,着重表现从一个活动到另一个 ...
- 关于 SVN 项目检出
前几天呢,同事遇到这么一个问题:他新建了一个工作空间,当他通过 svn 检出公司项目的时候,准备过来测试运行,但是呢出现了下面的报错 [ERROR] Failed to execute goal or ...
- 老生常谈:关于undo表空间的使用率
就在前几天,又有一个客户向我咨询undo表空间使用率的问题. 这让我想起几年前曾经有个省份的案例,客户的实际运维人员是一位刚毕业不久的女孩,几乎不懂Oracle原理,项目经理交给她的任务也是基础运维工 ...
- 数据结构--KMP算法总结
数据结构—KMP KMP算法用于解决两个字符串匹配的问题,但更多的时候用到的是next数组的含义,用到next数组的时候,大多是题目跟前后缀有关的 . 首先介绍KMP算法:(假定next数组已经学会, ...
- Disharmony Trees
/* 写完这篇博客有很多感慨,过去一段时间都是看完题解刷题,刷题,看会题解,没有了大一那个时候什么都不会的时候刷题的感觉,这个题做了一天半,从开始到结束都是从头开始自己构思的很有感觉,找回到当初的感觉 ...
- Java简单实现UDP和TCP
TCP实现 TCP协议需要在双方之间建立连接,通过输入输出流来进行数据的交换,建立需要通过三次握手,断开需要四次挥手,保证了数据的完整性,但传输效率也会相应的降低. 简单的TCP实现 //服务端 pu ...
- 关于C++中vector和set使用sort方法进行排序
C++中vector和set都是非常方便的容器, sort方法是algorithm头文件里的一个标准函数,能进行高效的排序,默认是按元素从小到大排序 将sort方法用到vector和set中能实现多种 ...