Socket通用型中转服务器
git源码地址
部署server前需要改变配置的部署ip和port,测试client指定ip和port
该模块在src/util/socket/下
服务端启动类 ServerTest.java
客户端测试类 client/ClientTest.java
关于socket模块在util.socket(为了复用很多工具,所以也没有单独抽离出来单独作为一个项目,就在以前的一个ssm/h的项目工具包里直接写了)
这个年代呢,socket服务开发多数公司都比较偏好采用第三方提供的解决方案,毕竟省时省力嘛,也比较可靠,稳定。
本文介绍 基于曾经从0编写的即时聊天系统所用到的socket服务器进行抽离业务适配,尝试设计一款通用性socket中转站(SO,类似路由器的路由规则),简图:
想要实现以下功能
- 系统服务端只需要负责和SO保持长连接
- 系统客户端只需要和SO保持长连接而非和系统服务端保持
- 系统客户端和服务端的交互消息都发往SO,SO负责根据消息头转发给目标
- 多个需要socket长连接交互的系统都可以共用该SO,可以跨系统消息交互
在下所理解的路由表简图:
使用效果
1)将SO部署到公网服务器上,192.168.1.1:8092
2)客户端连接SO,发送消息认证系统客户端
3)服务端连接SO,发送消息认证系统服务端
4)客户端发送普通消息到SO
5)服务端从SO收到该普通消息
6)服务端逻辑处理后发送消息给SO
7)目标客户端从SO收到该消息
public class Msg{
final public static int SHOW = -1000; //监控
final public static int BROADCAST = -2; //广播所有
final public static int BROADCAST_SYS = -1; //广播本系统
final public static int LOGIN = 0; //服务器/客户端登录
final public static int RES = 1; //发送结果提示用
final public static int TOSERVER = 11; //发往服务器
final public static int TOCLIENT = 12; //发往客户端
final public static int DATA = 10; //文本消息 请求转发
int msgType; //一条消息 的类型 登录系统Msg.LOGIN 广播本系统
String toSysKey; //发往目标系统 也可以根据socket绑定的sysKey 和 key做 逻辑验证
String toKey; //发往目标客户
String fromSysKey; //来自系统
String fromKey; //来自服务器
String info; //说明
String ok; //传输结果
Map data; //消息数据包
}
为了方便扩展,使用interface和abstract class的方式进行抽象多态
socket实现方式:
1)SocketIO
使用线程池1轮循读取每个长连接,使用线程池2发送消息,把每个读取和发送的操作认为是一个任务task,并实现异常重传,用线程池来避免大量线程消耗资源
2)SocketNIO
3)SocketNetty
使用Netty框架(基于SocketNIO),已经实现了异步线程池处理读取和发送事件,所以比较好用也比较稳定,只需要实现编码解码器并处理连接、断开、读取、发送事件即可
粘包分包解决方案
本系统采用基本的包头(<其实应该再在头部加上系统标识,实现移位丢弃无效字节功能>4字节,整个包长度)+消息体的方式
本系统针对于应用层面,所以并未做出严格的基于字节的数据传输编码解码(把非消息体的头全部编码为byte预读取,减少消息体的解析)优化,而是直接采用了JSON串解析来做为转发规则
附上SocketIO编码解码:
/**
* socket io 阻塞模式读取
*/
public static String readImpl(Socket socket) throws Exception {
String res = "";
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
if(isr.ready()){
byte[] head = new byte[4];
int read = is.read(head, 0, head.length); //尝试读取数据流 的头4个字节<int> 读取长度 -1表示读取到了数据流的末尾了;
if (read != -1) {
int size = Tools.bytes2int(head); //头4个字节 int 大小 int = 4byte = 32bit
int readCount = 0;
StringBuilder sb = new StringBuilder();
while (readCount < size) { //读取已知长度消息内容 异步读取 死循环 直到读够目标字节数
byte[] buffer = new byte[2048];
read = is.read(buffer, 0, Math.min(size - readCount, buffer.length) );
if (read != -1) {
readCount += read;
sb.append(new String(buffer,"UTF-8"));
}
}
res = sb.toString();
}
}
return res;
}
/**
* socket io 阻塞模式发送
*/
public static void sendImpl(Socket socket, String jsonstr) throws Exception {
if(!Tools.notNull(jsonstr))return;
byte[] bytes = jsonstr.getBytes();
OutputStream os = socket.getOutputStream();
os.write(Tools.int2bytes(bytes.length)); //int = 4byte = 32bit
os.write(bytes);
os.flush();
}
分组转发规则实现方式:
HashMap简易实现
HashMap<String, HashMap<String, ToClient<SOCK>>> toClients = new HashMap<String, HashMap<String, ToClient<SOCK>>>();
//使用HashMap来存放所有连接的引用,并建立sysKey 和 key 的索引,以便能够通过消息体中的toSysKey、toKey快速找到目标客户端长连接
/**
* 从底层传递上来的 收到的消息
* 此处负责找到该消息 对应的当前管理的 那个ToClient 并调用处理逻辑
* Arg -> socket -> ToClient -> sysKey,key
* str -> Msg(msgType, toSysKey, toKey, map)
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public void doMsg(SOCK sock, String jsonstr){
out("收到", jsonstr);
ToClient<SOCK> toClient = null;
ToClient<SOCK> fromClient = this.getClient(sock); //发送者<fromSys,from,socket>
if(fromClient == null) return;
String fromSysKey = fromClient.getSysKey(); //该发送者被记录认可的身份from
String fromKey = fromClient.getKey();
Msg msg = new Msg(jsonstr); //消息<fromSys,from,toSys,to>
msg.setFromKey(fromKey); //发消息者不需要设置自己的路由ip 只需要设置目标地点
msg.setFromSysKey(fromSysKey); //接收端会收到 来自那里kk 并发送自目标kk a - ss - s -k ss -k a -k ss - akb
if(msg.getMsgType() == Msg.SHOW){ //服务器/客户端登录 未登录某系统时 都归属于 this 0/1000+
msg.setFromKey(DEFAULT_KEY);
msg.setFromSysKey(DEFAULT_SYSKEY);
msg.setOk("true");
msg.setInfo("获取所有在线用户列表");
msg.put("res", show());
msg.setMsgType(Msg.RES);
send(sock, msg.getData()); //回传结果
}else if(msg.getMsgType() == Msg.BROADCAST){ //广播所有
msg.setOk("true");
msg.setInfo("广播 全系统");
for(String sysKey : toClients.keySet()){
for(String key : toClients.get(sysKey).keySet()){
send(toClients.get(sysKey).get(key).getSocket(), msg.getData());
}
}
}else if(msg.getMsgType() == Msg.LOGIN){ //服务器/客户端登录 未登录某系统时 都归属于 this 0/1000+
String newSysKey = msg.getToSysKey(); //登录目标系统 fromsyskey
String pwd = msg.getToKey(); //登录密码 fromkey
if(pwd.length() > 3){
this.changeServer(fromSysKey, fromKey, newSysKey);
}else{
this.changeClient(fromSysKey, fromKey, newSysKey);
}
show();
msg.setOk("true");
fromClient = this.getClient(sock);
msg.setFromSysKey(fromClient.getSysKey());
msg.setFromKey(fromClient.getKey());
msg.setMsgType(Msg.RES);
send(sock, msg.getData()); //回传结果
}else if(! fromSysKey.equals(DEFAULT_SYSKEY)){ //不属于默认管制 已经认证
out("解析结构", msg.getData());
if(msg.getMsgType() == Msg.DATA){
toClient = this.getClient(msg.getToSysKey(), msg.getToKey());
if(toClient == null){//不在线
msg.setOk("false");
msg.setInfo("不在线");
msg.setMsgType(Msg.RES);
send(sock, msg.getData());
}else{
send(toClient.getSocket(), msg.getData()); //发往目标
msg.setOk("true");
msg.setMsgType(Msg.RES);
send(sock, msg.getData()); //回传结果
}
}
}else{
msg.setOk("false");
msg.setInfo("Please login in, (msgType=0,toSysKey=sys001,toKey=pwd) ");
msg.setMsgType(Msg.RES);
send(sock, msg.getData());
}
}
模拟测试
测试多种实现方式的客户端连接多种实现方式的服务端 及其之间的信息转发
public class ServerTest {
public static void main(String[] args) {
// new ServerHashmapImpl(new SocketIO()).start();
// new ServerHashmapImpl(new SocketNIO()).start();
new ServerHashmapImpl(new SocketNetty()).start();
}
}
public class ClientTest {
public static void main(String[] args) {
// new ClientUI(new ClientIO("127.0.0.1", 8090), "io-io");
// new ClientUI(new ClientIO("127.0.0.1", 8091), "io-nio");
// new ClientUI(new ClientNIO("127.0.0.1", 8090), "nio-io");
// new ClientUI(new ClientNIO("127.0.0.1", 8091), "nio-nio-server");
// new ClientUI(new ClientNIO("127.0.0.1", 8091), "nio-nio-client");
// new ClientUI(new ClientNIO("127.0.0.1", 8092), "nio-netty-client");
new ClientUI(new ClientNetty("127.0.0.1", 8092), "netty-netty-client");
}
}
最后
经测试,SocketIO的实现方式有些许问题,时间久了之后,cpu和内存的使用异常,还是哪里设计出了些问题,SocketNetty的实现方式就很稳定了,毕竟公认几大实用socket框架,怎么说呢,虽然常说不要重复造轮子,但是呢造造轮子也可以帮助更好的使用轮子,也还能稍微有些成就感呢,同时也能发现自己设计的局限性,开拓一下见识,你不去尝试自己实现一下都不知道别人的项目有多厉害哎