Socket通用型中转服务器

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框架,怎么说呢,虽然常说不要重复造轮子,但是呢造造轮子也可以帮助更好的使用轮子,也还能稍微有些成就感呢,同时也能发现自己设计的局限性,开拓一下见识,你不去尝试自己实现一下都不知道别人的项目有多厉害哎

猜你喜欢

转载自blog.csdn.net/u014303844/article/details/80235339