纯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多线程编程中的常见陷阱,如在构造函数中启动线程,不完全的同步 ...
随机推荐
- 使用 Prometheus + Grafana 对 Kubernetes 进行性能监控的实践
1 什么是 Kubernetes? Kubernetes 是 Google 开源的容器集群管理系统,其管理操作包括部署,调度和节点集群间扩展等. 如下图所示为目前 Kubernetes 的架构图,由 ...
- formData 无需form异步上传多个图片
上周帮其它公司套一下一个web端微信投票系统的后台接口,遇到了一个图片以及视频上传报名的小问题,网上实现方式有很多但都不是ui上面的效果,于是自己动手改造了一个.先来看看效果图 流程很简单,就是点击每 ...
- gnome 3 美化
首先,去http://gnome-look.org/找到需要的主题,然后手动安装或者用下载到的主题包里的脚本安装 手动安装对应路径如下: 鼠标,图标主题解压放置:~/.icons或/usr/share ...
- viewpager的滑动
在一个已经是月黑风高快下班的时刻了,我们产品突然通知我们开会,要添加一个功能,他闲来无聊随便戳了戳facebook,说点开联系人的那个横向滑动的卡片式的效果不错,让我们在我们的app里添加这个效果,我 ...
- 数据帧CRC32校验算法实现
本文设计思想采用明德扬至简设计法.由于本人项目需要进行光纤数据传输,为了保证通信质量要对数据进行校验.在校验算法中,最简单最成熟的非CRC校验莫属了. 得出一个数的CRC校验码还是比较简单的: 选定一 ...
- vi 编辑器笔记
摘要: vi从安装到使用 vi从菜鸟到高手 0. vim - Vi IMproved, a programmers text editor 分为 VI和VIM,现在流行的发行版里面VI=VIM 是一个 ...
- python参考手册一书笔记之第一篇上
在python2和python3的版本差异很大输出hello world的方法在2里支持在3里就不支持了. print 'hello world' #在2中支持 print ('hello world ...
- Quart.Net分布式任务管理平台
无关主题:一段时间没有更新文章了,与自己心里的坚持还是背驰,虽然这期间在公司做了统计分析,由于资源分配问题,自己或多或少的原因,确实拖得有点久了,自己这段时间也有点松懈,借口就不说那么多 ...
- Java基础——数据类型
Java中与C++的区别: 1.Java中没有无符号类型. 2.整型值和布尔值之间不能进行相互转换. 3.Java中不区分变量的定义和声明. 如:在C++中int i = 10;是一个定义,而exte ...
- 磁盘管理之 raid 文件系统 分区
第1章 RAID 磁盘阵列 1.1 使用raid的目的 1)获得更大的容量 2)让数据更安全 3)读写速度更快 1.2 raid0.raid1.raid5.raid10对比 磁头 0磁道 1扇区 前4 ...