Socket编程技术
1.基本原理
本文记录对Socket通讯技术的汇总,现在想对.NET/C#程序员说:想要掌握异步Socket通讯技术,首先应该掌握C#语言里的异步编程,然后再学习Socket可能会容易理解,这里有特别强调了异步Socket通讯,因为当下生产环境基本上没人再使用同步实现了。本文主要记录TCP/IP协议的Socket通讯,不包括UDP协议的Socket通讯。
1.1.I/O完成端口(IOCP)
IOCP全称I/O Completion Port,它是Windows平台下异步通讯实现的方式之一,相对于其他几种实现机制,它是高效的、使用简单的,但是内部实现是复杂的。在Windows平台下想要打造一款高性能服务端程序,没有比它更合适了。它特别适合C/S模式网络服务器端模型,在处理大量用户并发请求时,如果采用一个用户对一个线程的方式,那么将造成CPU在这成千上万的线程间来回切换,后果不堪设想。而IOCP不会为每一个用户创建一个线程。
IOCP模型包含三部分:完成端口(存放重叠I/O请求),客户端请求处理,工作线程,首先解释一下几个概念
- 重叠结构Overlapped:它是一个很重要的I/O数据结构,Windows里所有的异步通信实现都是基于它。
可以把它理解成为一个网络操作的ID,我们利用重叠I/O提供的异步机制,每一个网络操作都要有一个唯一的ID,因为进入系统内核后,就由系统内核控制了,在外面不清楚里面在做什么,系统内核一看到有重叠I/O的调用进来,就会使用其异步机制,并且操作系统就只能靠这个重叠结构带有的ID来区分是哪一个网络操作,然后内核里面处理完毕后,根据这个ID继续操作。
至于为什么会叫重叠结构,其作者解释是因为“执行I/O请求的时间与线程执行其他任务的时间是重叠的”。 - 完成端口:有人说叫它“完成队列”更合适,因为这里的端口和我们平时所说的网络通讯中的端口完全不是一个东西,实际上IOCP对象内部有一个先进先出队列(简称消息队列)。它之所以叫“完成”端口,就是说系统会在网络I/O操作“完成”之后才会通知我们,即我们在接收到系统通知的时候,其实网络操作已经完成了。
- 工作线程:它是专门用来和客户端进行通信的,而且工作线程的数量要等于系统中CPU的数量(CPU的核心数)。但是实践证实最好建立CPU核心数*2数量的工作线程,这样便可以充分利用CPU资源,因为工作线程有时会出现Sleep()等情况,此时同一个CPU核心上的另一个线程就可以代替这个Sleep的线程执行了。
通常情况下,我们会用线程池维护工作线程,一个IOCP对象,在操作系统中可关联着多个Socket或文件控制端。IOCP对象内部的消息队列,用于存放IOCP所关联的I/O服务请求完成消息。请求I/O服务的进程不接受I/O服务完成通知,而是检查IOCP的消息队列以确定I/O请求的状态。IOCP队列中的请求完成后,应用程序会收到通知。工作线程负责从IOCP消息队列中取走完成通知并执行数据处理。如果队列中没有消息,那么线程阻塞挂起在该队列,不占用CPU周期,工作线程从而实现负载均衡。
IOCP模型之所以是Windows平台下C/S通信模式中性能最好的网络通讯模型,是因为它充分利用Windows内核来进行I/O的调度。它实现高性能、高并发可以概括为以下三点:
- 采用异步I/O操作,弥补了同步操作线程阻塞耗时的缺点。
- 采用线程池进行处理,减少了线程创建、上下文切换占用过多系统资源的问题。
- 采用重叠I/O技术,帮助维持可以重复使用内存池。
1.2.通信原理
1.2.1.Socket模式
Socket技术有两种编程模式:同步模式和异步模式,我们常常把它们混淆为阻塞和非阻塞,前者是指通信模式,后者则指是否等待I/O操作完成。I/O操作相对于CPU的执行效率犹如老牛破车,I/O操作主要指网络I/O和存储设备I/O。
同步模式是最基本的Socket编程模式,正如其名,同步模式的Socket在执行耗时的I/O操作时,会阻塞当前线程,等待I/O操作完成返回结果,否则停止向下执行。
异步模式是相对于同步模式定义的,它在执行过程中,遭遇耗时的I/O操作时,不会阻塞当前线程,它会向系统委托一个异步过程,然后继续向下执行,当系统接收到I/O操作完成的消息后,系统会自动触发委托的异步过程,从而完成一个完整的流程。
强调一个概念,Socket编程的同步和异步,与线程间的同步不是一个概念,线程间的同步指不同线程具有先后关联关系,而Socket同步和异步指两种不同的工作方式。
1.2.2.Socket原理
编写网络通信程序,首先想到的应该是OSI七层协议模型,或者是TCP/IP五层协议模型,不过无论是OSI7还是TCP/IP5都不存在Socket这个概念?
如上图所示,Socket是基于TCP/IP协议族封装的一套标准规范的、可调用的接口,它介于应用层与运输层中间,由于TCP/IP协议族的概念和实现过于复杂,所以就将它们抽象接口化,抽象接口与其协议一一对应,最后开发者只需要调用接口即可使用。
由于Socket是在TCP/IP协议族上工作的,所以要想实现和灵活运用这门技术,必须要理解TCP/IP协议是如何通讯的,即TCP/IP协议三次握手建立连接,和四次握手释放连接,如下图所示:
1.3.通信程序
上图内容展示了socket客户端和服务端程序通信建立的步骤和过程。
1.3.1.服务端
- 创建套接字:new Socket()
查看C#中Socket类的构造函数提供了三种创建方式,主要分析常用的、传递三个参数的构造函数
函数签名:public Socket(AddressFamily af, SocketType st, ProtocolType pt);
参数说明:- AddressFamily:地址族,常用的地址族有InterNetwork、InterNetworkV6、Unix,它决定了socket的地址类型,在通信中必须采用对应的地址。
- SocketType:类型,常用的类型有Stream、Dgram、Raw。
- ProtocolType:协议类型,常用的协议有TCP、UDP、ICMP。
- 指定本地地址:socket.Bind()
方法签名:public void Bind(EndPoint localEP);
参数说明:此方法传递EndPoint对象,并没有给传递字符串的机会,避开了传递IP地址时可能会出现的大小端的问题。 - 监听连接:socket.Listen()
方法签名:public void Listen(int backlog);
参数说明:backlog表示请求连接队列的最大长度,用于限制排队请求的个数,即最多允许多少个客户端接入。 - 接收连接:socket.Accept()
- 在.NET/C#中,Socket编程之所以会有几种实现方案,是因为异步编程的发展。而不同实现方案的切入点,服务端就是从这里开始的,所以这一步非常重要。
- 同步实现:public Socket Accept();
- 异步实现
- APM模式:就是通过“BeginXXX/EndXXX”方法对回调函数方式实现的,由于采用这种模式是会产生一个IAsyncResult托管对象,会带来GC方面回收压力,特别是消息收发时。这就不符合打造高性能服务器的要求,所以现在主要使用EAP模式的Socket通信。Socket类提供了三个不同参数的BeginAccept方法,不过大家还是习惯使用两个参数的方法,其签名如下:
public IAsyncResult BeginAccept(AsyncCallback callback, object state); - EAP模式:它是基于SocketAsyncEventArgs类实现Socket通信,所以要了解更多就去研究SocketAsyncEventArgs类,EAP模式对应的Accept()方法签名是:
public bool AcceptAsync(SocketAsyncEventArgs e); - TAP模式:Socket对象不直接支持TAP模式,而是通过TcpListener提供TAP模式的实现。在.NET Core或.NET5+的环境里支持TAP模式。
- APM模式:就是通过“BeginXXX/EndXXX”方法对回调函数方式实现的,由于采用这种模式是会产生一个IAsyncResult托管对象,会带来GC方面回收压力,特别是消息收发时。这就不符合打造高性能服务器的要求,所以现在主要使用EAP模式的Socket通信。Socket类提供了三个不同参数的BeginAccept方法,不过大家还是习惯使用两个参数的方法,其签名如下:
- 消息收发:socket.Send()/socket.Receive()
- 与Socket.Accept()方法相同,消息收发方法根据异步编程的发展提供了几种实现方案。实现消息的收发是Socket通信的根本目标,所以这一步是核心。
- 同步实现:
- 发送消息方法:Socket类提供了8个参数不同的Send()方法,常用其中两个方法
- 发送短消息:public int Send(byte[] buffer, SocketFlags socketFlags);
- 发送长消息:public int Send(byte[] buffer, int offset, int size, SocketFlags socketFlags);
- 接收消息方法:Socket类也提供了8个参数不同的Receive()方式,常用方法签名:public int Receive(byte[] buffer);
- 发送消息方法:Socket类提供了8个参数不同的Send()方法,常用其中两个方法
- 异步实现:
- APM模式:
- 发送消息方法:Socket类提供了多种实现方法,常用方法签名:
public IAsyncResult BeginSend(byte[] b, int o, int s, SocketFlags sf, AsyncCallback cb, object state); - 接收消息方法:Socket类提供了多种实现方法,常用方法签名:
public IAsyncResult BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state);
- 发送消息方法:Socket类提供了多种实现方法,常用方法签名:
- EAP模式:
- 发送消息方法签名:public bool SendAsync(SocketAsyncEventArgs e);
- 接收消息方法签名:public bool ReceiveAsync(SocketAsyncEventArgs e);
- TAP模式:Socket对象不直接支持TAP模式,而是通过TcpListener提供TAP模式的实现。在.NET Core或.NET5+的环境里支持TAP模式。
- APM模式:
- 关闭客户端连接:socket.CloseClient()
- 此步骤并没有在客户端-服务端Socket通信流程图中出现,主要为了更好的介绍两端本身,实际上本步骤非常重要,因为服务端的资源非常昂贵、且需要提供可靠的稳定性服务。
- 在客户端与服务端建立连接后,客户端需要设计心跳功能,在没有发送或接受消息时,让服务端Socket知道,自己仍处于活跃状态。否则服务端为了充分节约计算机资源而释放客户端的连接。
- 客户端与服务端建立的连接,还会因为各种不可控因素(人为、非人为)断开,所以一定要做好异常处理。否则会造成程序崩溃、服务不可用。
- 关闭套接字:socket.Close()
执行该步骤前,应当优雅的关闭所有与客户端建立的连接,避免消息丢失,以及造成客户端程序异常、不可用。
1.3.2.客户端
- 创建套接字:new Socket()
查看C#中Socket类的构造函数提供了三种创建方式,主要分析常用的、传递三个参数的构造函数
函数签名:public Socket(AddressFamily af, SocketType st, ProtocolType pt);
参数说明:- AddressFamily:地址族,常用的地址族有InterNetwork、InterNetworkV6、Unix,它决定了socket的地址类型,在通信中必须采用对应的地址。
- SocketType:类型,常用的类型有Stream、Dgram、Raw。
- ProtocolType:协议类型,常用的协议有TCP、UDP、ICMP。
- 建立连接:socket.Connect()
- 服务端被动接收客户端发起的建立Socket连接请求,所以此处与服务端不同。也是因为受同步和异步编程的影响,也存在三种实现方案。
- 同步实现:Socket类提供了4种参数不同的Connect()方法。注意:程序执行此方法时,可能会出现异常,如果出现异常了,可能涉及到了“字节序”的概念,如有需要可看本文“基础原理-扩展信息-字节序”部分内容。
- 异步实现
- APM模式:Socket类提供了4中参数不同的BeginConnect()方法
- EAP模式:它是基于SocketAsyncEventArgs类实现Socket通信,所以要了解更多就去研究SocketAsyncEventArgs类,EAP模式对应的Accept()方法签名是:public bool ConnectAsync(SocketAsyncEventArgs e);
- TAP模式:Socket对象不直接支持TAP模式,而是通过TcpListener提供TAP模式的实现。在.NET Core或.NET5+的环境里支持TAP模式。
- 消息收发:socket.Send()/socket.Receive()
客户端消息收发与服务端消息收发的实现没有不同,所以此处不再赘述。 - 关闭套接字: socket.Close()
对于基于Socket通信的程序来说,关闭Socket连接,表示关闭程序了,所以需要两端都要释放用户申请的资源。
1.4.通信安全
在.NET/C#下,采用Socket+异步编程打造高性能程序,有三种方案可以选择:APM、EAP和TAP,在上述内容中提到了采用EAP模式更好。但是EAP模式不支持消息的安全传输,即消息发送前或接收后SSL/TLS的加解密,这可能是个麻烦的事情,当然你可以自己对消息进行加解密实现,但终究不是通用的解决方案,特别是遭遇多方相互通信时。
鉴于EAP模式不具备安全传输消息的遗憾,因此,如果你要进行安全通信,就要选择TAP模式了。
1.5.扩展信息
1.5.1.字节序
现在做socket编程时,基本上很少遭遇“字节序”问题,这源于UTF-8编码的盛行。想要把字节序的概念搞清楚,需要花费一定的时间和精力查查历史,以及学学涉及到的几个其他概念,比如:字符编码、大端小端、编程语言对大小端默认选择、CPU对大小端默认选择、字节序的种类等,有没有出现恐慌的心理和情绪变化?不要慌、也不要激动,不懂不要紧、忘了也不要紧,接下来一个个说。
1.5.1.1.字符编码
我们知道计算机只能识别010101这样的字符串,字母、数字、汉字、图片、音视频等等最终都需要转换成01字符串,当然需要制定转换规则,否则就乱套了。计算机是老美发明的,所以刚开始的时候就设计出了ASCII字符集,它的全称American Standard Code for Information Interchange,中文名称美国信息交换标准码,它使用7 bits来表示一个字符,总共表示128个字符,我们一般都是用字节(byte,即8个01串)来作为基本单位.那么怎么当用一个字节来表示字符时第一个bit总是0,剩下的七个字节就来表示实际内容。后来IBM公司在此基础上进行了扩展,用8bit来表示一个字符,总共可以表示256个字符.也就是当第一个bit是0时仍表示之前那些常用的字符.当为1时就表示其他补充的字符。
对于说英文的美国来说,256个字符是使用不完的,但是对于说其他语言的国家就不行了,就像我们的汉字有几万个字。于是就出现Unicode和ISO这样的组织来统一制定一个标准,使得任何一个字符只对应一个确定的数字.ISO取名字叫UCS(Universal Character Set),Unicode取的名字就叫unicode。接下来就详细说说unicode编码。
- Unicode版本
Unicode编码有两个版本。第一个版本规定用两个字节来表示所有字符,总计可以表示65535个字符, 65535是2的16次方,所以常常会把Unicode编码等同于UTF-16。
后来在发现第一个版本65535不算多,要是加上特殊的字符就不够了,于是从1996年开始制定了第二个版本。第二个版本规定用四个字节来表示所有字符,所以就出现了UTF-32。
UTF-8是怎么出现的?在制定Unicode第一个版本时,就发现英文一个字节就可以表示了,为什么要浪费两个字节表示呢,所以就出现UTF-8。看到8/16/32,可能误以为8表示一个字节,不是的。
再说一点,Unicode编码规范是给所有字符指定一个唯一的数字,如何把数字转换成01串保存到计算机中,就有不同的方式了,所以才出现了UTF-8/UTF-16/UTF-32的出现。 - UTF-8/UTF-16
当用UTF-8时表示一个字符是可变长度,有可能是用一个字节表示一个字符,也可能是两个、三个、甚至是四个,反正是根据字符对应的数字大小来确定。
举例说明两者的区别,假如中文字"汉"对应的unicode是6C49(十六进制,十进制是27721)- UTF-16表示:就是01101100 01001001(共16 bit,两个字节).程序解析的时候知道是UTF-16就把两个字节当成一个单元来解析。
- UTF-8表示:就是1110xxxx 10xxxxxx 10xxxxxx(共24bit,三个字节)。
换算关系:
一个字节只能表示2的7次方128个字符
两个字节只能表示2的11次方2048个字符
三个字节能表示2的16次方65536个字符
因为27721>2048,所以需要三个字节表示。所以开头有三个数字111。
- 如何区分文本内容采用哪种编码方式?
- EF BB BF UTF-8
- FE FF UTF-16/UCS-2, little endian
- FF FE UTF-16/UCS-2, big endian
- FF FE 00 00 UTF-32/UCS-4, little endian
- 00 00 FE FF UTF-32/UCS-4, big-endian
根据上面的映射关系,在文件开头处对照看看就知道了。
1.5.1.2.字节序种类
字节序有两种类型:主机字节序 和 网络字节序。
主机字节序:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。
网络字节序:TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big-endian(大端)排序方式。
1.5.1.3.大端小端
- 大端小端的定义
- 大端字节序(Big-Endian,简称大端):就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
- 小端字节序(Little-Endian,简称小端):就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
- 如何更好的理解大端小端?
- 名称由来
1726年的Jonathan Swift的《格列佛游记》,其中一篇讲到有两个国家因为吃鸡蛋究竟是先打破较大的一端还是先打破较小的一端而争执不休,甚至爆发了战争。1981年10月,Danny Cohen的文章《论圣战以及对和平的祈祷》(On holy wars and a plea for peace)将这一对词语引入了计算机界。 - 高地址和低地址
计算机内存地址是有编号的,从小到大进行编号,编号小的地址相对于编号大的地址称为低地址,反过来编号大的地址就被称为高地址。 - 高位字节和低位字节
举例说明:int a=16777220,换算成十六进制是0x01000004,相对来说,04就是低位字节,01则是高位字节 - 示例
- 16bit宽的数0x1234在内存中的存放方式
内存地址
小端模式存放顺序
大端模式存放顺序
0x4000
34
12
0x4001
12
34
- 32bit宽的数0x12345678在内存中的存放方式
内存地址
小端模式存放顺序
大端模式存放顺序
0x4000
78
12
0x4001
56
34
0x4002
34
56
0x4003
12
78
- 16bit宽的数0x1234在内存中的存放方式
- 总结
经过上述四步分析,应该理解什么是大端小端了吧。其实就是数据在内存或外部设备存储顺序。
- 名称由来
- 为什么要区分大端小端?
因为在计算机系统中,数据是以字节为单位的,每个地址单元都对应着一个字节,一个字节8bit。但是在C语言中除了8bit的char外,还有16bit的short型、32bit的long型。对于位数大于8位的处理器,如16位或32位处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节排序的问题 - 大端小端各自优势
- 大端字节序的优势:符合人类的阅读习惯。
- 小端字节序的优势:因为计算机电路先处理低位字节,效率比较高,计算都是从低位开始的,所以计算机的内部处理都是小端字节序。
- 其他信息
其实,处理器在读取外部设备的数据,处理字节序的时候,并不知道什么是高位字节,什么是地位字节,它只知道按照顺序读取字节。即使是向外部设备写入数据,也不用考虑字节序,正常写入一个值即可,外部设备会自己处理字节序的问题。
1.5.1.4.编程语言对大小端默认值
下述列表测试平台为Windows10操作系统 和 Intel CPU i7。
1) VB6:小端
2) C:小端
3) C++:小端
4) C#:小端
5) Golang:小端
6) STM32:小端
7) C51&C52:大端
8) Java:大端
所以在与C51&C52和Java程序进行Socket通信时,一定要注意大小端问题。
1.5.1.5.处理器对大小端默认值
1) x86,MOS Technology 6502,Z80,VAX,PDP-11等处理器为Little endian
2) Motorola 6800,Motorola 68000,PowerPC 970,System/370,SPARC(除V9外)等处理器为Big endian
3) ARM, PowerPC (除PowerPC 970外), DEC Alpha, SPARC V9, MIPS, PA-RISC and IA64的字节序是可配置的
1.5.1.6.字符编码与大小端的关系
从上面1到5个主题看,并没有看出字符编码与大小端有直接关系,现在就举个有关系的例子。
题目1:使用C#语言编写一个Socket程序,与C51机器进行网络通信,双方传输的数据采用Unicode编码(UTF-16),C#程序通过System.Text.Encoding.Unicode.GetBytes(string str)方法转换字符串为byte数据。
测试:你可能会惊奇的发现,双方无法通讯。
原因:这是因为System.Text.Encoding.Unicode.GetBytes()换出来的byte数组是小端字节序,而C51那边是大端字节序。其实System.Text.Encoding对象下还提供了BigEndianUnicode.GetBytes()方法。
题目2:在题目1的基础上修改编码方式,双方都改为GB2312或UTF-8
测试:没有问题
原因:为什么呢?因为gbk和utf-8编码都是以单个字节表示数字的,不存在字节序的问题,而UTF-16编码是以双字节表示一个数字,因此会有字节序问题,即大小端,所以会受影响。
现在知道字符编码与大小端也是有关系的。
1.5.1.7.总结
经过上述分析汇总,应该清楚了,在进行网络通信编程时,如果出现了跨平台、跨语言的场景,一定要注意字节序的影响,除此之外还要注意字符编码、通讯协议的实现、驱动程序等也会出现字节序的问题,影响开发调试进度。
2.实现方案
在.NET/C#中对Socket的支持都是基于Windows I/O Completion Port完成端口技术的封装,通过不同的Non-Blocking封装结构满足不同的编程需求。如果从异步编程的角度看,就是对不同时期异步编程的支持。
2.1.APM模式
以下内容等想写了再写,有些不快了吧,哈哈哈……
2.2.EAP模式
以下内容等想写了再写,有些不快了吧,哈哈哈……
2.3.TAP模式
以下内容等想写了再写,有些不快了吧,哈哈哈……
3.技术设计
3.1.功能设计
3.1.1.消息边界
3.1.1.1.TCP和UDP协议
在Socket网络编程中,TCP是面向连接的传输层协议,它的目标是提供可靠的端到端连接,保证消息有序无误的传输。为了提升消息的网络传输效率,消息发送端发送消息时,采用合并算法,将发送间隔短、数据量小的多包数据合并成为一大包数据后传输。消息接收端接收到消息后,采用对应的拆解算法,把大包数据拆解还原为多包数据。这种合并与拆解操作又被称为封包和拆包。
UDP不是面向连接的协议,提供一对多的消息传输服务,它没有使用合并与拆解算法优化数据包。它的消息接收端采用了链式结构存储每一包接收的数据,且每包数据都带有消息头,其中包含来源地址和端口等信息,所以UDP通信不存在粘包问题。
3.1.1.2.保护消息边界和流
保护消息边界指传输协议把数据封装成一包独立的消息在网上传输,接收端只能接收到一包独立的消息,也就是说发送端发送一包数据,接收端一次只能接收一包数据,所以又被称为面向消息传输。无保护消息边界指传输协议把数据视为一串字节流,接收端在一次接收操作还原数据时,可能会发现接收了多包数据,因此又被称为面向流式传输。
TCP为了保证可靠传输,减少额外开销,采用面向流式传输。面向流式传输可以减少数据包的发送数量,从而减少了如数据包验证等操作额外开销。由于TCP协议通过合并数据包的方式传输,对于数据传输频繁的需求来说,就会出现粘包问题,同时也会增加接收端拆包的工作量。所以TCP面向流式传输方式,更适合对数据传输要求可靠、且不需要太频繁传输的场景。
UDP是面向消息传输,不存在粘包问题。但是当发送的数据包(有效载荷)大小较小时,就会因为发送次数的增加造成发送端和接收端的资源开销,如系统调度、硬件设备等。同时由于在网络上传输数据包的次数相对TCP来说增多了,所以可靠性也就降低了。因此UDP面向消息传输方式,更适合面向大数据包(不大于UDP协议最大载荷),且对可靠性要求没那么严苛的场景。
3.1.1.3.解决粘包问题
基于TCP协议实现的面向流式传输的数据包,之所以出现粘包问题,既有可能因为发送方合并多包消息、或发送频率过快造成的,也有可能因为接收方处理消息速度太慢,导致缓冲区里的多包数据连在了一起引发的。
解决TCP粘包问题,一般会采用以下三种方式,可以根据实际场景选择不同的方式。
- 发送固定长度的消息
- 优势:容易简单,只要通信双方都按照固定长度发送和接收数据即可。
- 缺陷:由于长度是固定的,长度值会使数据包的数量增加或发送延迟,比如原始包较大,需要按照长度值拆分为多次发送;原始包没有达到长度值,延迟发送。
- 消息长度和消息一起发送
- 优势:容易简单,适合任何场景。不存在发送固定长度消息的多次发送和延迟问题。
- 缺陷:这种方式通常是在消息前面增加固定几个字节表示消息长度,所以通讯双方需要提前协商好消息长度占用的字节数,如4个字节。
- 使用特殊标记处理(分隔符)
- 优势:扩展性强,适合任何场景。不存在第二步中协商消息长度问题,也不存在第一步中多次发送和延迟问题,只需要通信双方按照约定的分隔符对数据进行封包和拆包传输即可。
- 缺陷:要求消息体中不能出现和分隔符相同的字符串,所以通常采用的处理方式是对消息体进行编码,编码自然会增加系统资源的开销。
3.1.2. 消息编码
3.1.2.1.字符编码
如果采用特殊标记解决TCP传输的粘包问题,通常的做法是使用Base64编码方式对要传输的数据进行编码,保证编码后的数据不会出现特殊标记,当然也可以自定义更高效的编码方式。
3.1.2.2.加解密
此处提到加解密是指在进入Socket编程之前实现对数据加密,以及数据结束Socket编程之后进行解密。这主要是因为EAP模式不支持SSL/TLS方式的安全传输,不过这样做也不好,如果实现方式不好会降低效率(业务层面,不影响Socket)。
3.1.2.3.解压缩
不论技术如何发展,网络通信带宽资源都是非常宝贵的,所以对传输数据进行压缩任何时期都是必要的,特别对于某些行业来说通信带宽极其珍贵,比如通过卫星通信的航海、军事等行业,通信带宽依然处于2Mbit/s,即256kb/s。
所以要像加解密操作一样,在进入Socket处理之前需要对数据进行压缩,以及Socket处理完之后进行解压缩。
3.2.产品设计
3.2.1. 上位机设计
在设计上位机程序时,很多人喜欢将UI可视化交互调控功能和上位机网络通信功能放在一起,个人认为这是不严谨的、不规范的,不要求把上位机程序设计成为消息服务器,但至少要将上述两大功能分为两个程序进行设计,最好将网络通信功能做成系统服务程序运行在后台,这样就会降低和减少UI操作对网络通信功能的影响。
如果将UI交互调控与网络通信传输分为两个程序,就会遇到进程间通信的问题,这就涉及到进程间通信技术了。进程间通信技术有多种实现方案,如:共享内存、命名管道和匿名管道、发送消息、Socket通信等。
- 共享内存:就会遇到另一个问题:数据同步。可以使用“互斥量Mutex”、“信号量Semaphore”或“事件Event”解决。
- 优点:比较适合两个进程间大数据量的交换。
-
缺点:仅限于同一台计算机。且设计到非托管代码和资源操作,可能会出现安全问题。
- Remoting:它是通过通道(channel)来实现两个应用程序域之间对象通信的。
- 优点:利用TCP通道速度非常快,可以远程调用。
- 缺点:不是标准的技术,对平台等有依赖性;传输的数据会被序列化,会使性能下降。
- 命名管道:它是一种从一个进程到另一个进程用内核对象来进行信息传输。和一般的管道不同,命名管道可以被不同进程以不同的方式方法调用(可以跨权限、跨语言、跨平台)。
- 优点:不会对数据进行序列化。
- 缺点:仅限于同一台计算机或同一局域网内部。且受网卡质量和性能的影响。通讯时没有安全层。
- 发送消息:利用系统SendMessage和PostMessage等函数实现进程间通讯。
- 优点:速度快、效率高。
- 缺点:仅限于同一台计算机。且需要获取消息接收方的窗口句柄,显然不适合系统服务。发送消息还会受消息接收方窗口状态的影响。
- Socket通信:应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,把复杂的TCP/IP协议族隐藏在Socket接口后面。通过IP地址和端口进行数据的传输。
- 优点:不受地理位置和网络空间的限制,更加灵活。
- 缺点:受网卡质量和性能的影响。
综上所述,从企业发展、灵活运用、技术服务等多角度分析,得出结论是:还是基于消息服务器的模式,并采用Socket技术,进行上位机程序的设计和研发会更有现实意义和价值。
3.2.2.消息服务器设计
一提到消息服务器,你首先想到的应该是QQ和WeChat吧?它们算是即时通信领域里的王者。不过作为一名码农,你可能更想知道如何设计出像它们一样强大的消息服务平台,面对亿万级用户量的即时通信,与工业领域中规模有限的上位机服务器远不在一个层次。即时通信的用户是双向互发消息,不同于主要负责单向接受消息的上位机。面对真正海量用户,必须设计专门的通信协议、通信模型,和具有完整性、伸缩性的程序架构、服务架构、存储架构,以及管理运维架构,否则难以支撑海量用户的通信需求。即时通信领域有标准的通信协议XMPP,大多数的即时通信软件协议都是基于它,或是在其基础上定制的。
即时通信从功能管理上可分为三部分:消息管理,服务管理,功能应用,以下将进一步它们的具体功能设计。
- 消息管理
- 消息服务器转发器:负责将消息转发给目标用户。
- 消息服务器存储器:负责将消息写入数据库持久化存储。
- 服务管理
- 消息服务器管理:管理和维护消息服务器资源。
- 地理区域管理:负责地理空间和行政区域划分下的消息服务器管理。
- 跨网通讯管理:更多是指国家间的互联互通。
- 安全通讯管理:涉及通信安全的技术、行政、信仰、文化等安全控制。
- 文件存储管理:用于存储通信双方传递的文件,如图片、音视频、文本文档等。
- 功能应用
- 自我管理:信息维护、状态管理、所属消息服务器位置等。
- 朋友管理:信息维护、状态管理、所属消息服务器位置等。
- 群组管理:信息维护、成员管理、所属消息服务器位置等。
- 查找管理:添加好友,创建/加入群组。
- 消息管理:已读/未读,文本字符/图片/音视频文件,历史消息管理
- 通知管理:消息发送和接受时的通知方式等。
上述内容简单说明了,即时通信系统的重点对象和功能管理,接下来就说说工作流程设计。首先要说明一下采用类似于电子邮件方式设计,这种方式属于无主机模式,而且具有很强的弹性调控,主要分为五个步骤,如下:
- 身份鉴别过程
参数(权鉴服务器地址,ID和密码),用户使用客户端程序登录权鉴服务器,权鉴程序验证ID是否存在,存在则继续验证密码是否正确,最后向客户端输出结果。 - 申请消息服务器
登录成功,表示客户端有资格申请消息服务器,客户端到资源分配服务器申请消息服务器。资源服务器依据实际情况分配消息服务器。 - 资源服务器
资源池维护着可提供服务的消息服务器数量和信息,按照就近原则资源服务器为客户端用户分配消息服务器,消息服务器可服务的客户端数量*消息服务器数量,就是服务能力上限,也是资源池的最大值。资源池的大小可通过增减消息服务器的数量和服务能力动态调控。 - P2P即时通信
客户端用户申请到消息服务器,便可以和好友进行即时通信。但是与好友通讯前,首先需要知道好友所在的消息服务器。 - 群组即时通信
客户端用户要查询群组的信息,同时也需要把自己的消息服务器地址告诉群组消息服务器,这样就可以和群组中的人进行群聊。
通过对上述功能管理和工作流程的设计与分析,是不是对即时通信系统的设计有点感觉了,看似简单的一款聊天软件,想不到其架构设计如此深奥吧。即时通信软件最适合锻炼和检验socket编程技术能力,而大多数程序员都是基于业务需求做功能性的应用开发,工作中很少会涉及到网络通信层面,所以经常会有人谈socket色变的情况。其实还好啦,静下心来慢慢啃,总会有收获的。
4.扩展信息
4.1.XMPP
XMPP是一种基于标准通用标记语言的子集XML的协议,它继承了在XML环境中灵活的发展性。因此,基于XMPP的应用具有超强的可扩展性。经过扩展以后的XMPP可以通过发送扩展的信息来处理用户的需求,以及在XMPP的顶端建立如内容发布系统和基于地址的服务等应用程序。而且,XMPP包含了针对服务器端的软件协议,使之能与另一个进行通话,这使得开发者更容易建立客户应用程序或给一个配好系统添加功能。
如果做即时通信消息服务器软件,那么XMPP协议是你应该非常清楚的,它就像Http协议对于浏览器一样重要。
XMPP官网:https://xmpp.org/
4.2.WebSocket
因为WebSocket本身也是基于Socket实现的,所以可以尝试基于已经实现的EAP模式或TAP模式的Socket功能进行扩展。像其他协议也可以在此基础上实现,如Http/Https,FTP等等。
5.总结
最近几年忙于云平台和架构的设计与研发,现在终于有时间来整理一下知识点了。一开始没有整理这篇文章的计划,搜了半天文章,感觉都不是很全面,为此就有了写此文的想法。
6.参考信息
IOCP:https://blog.csdn.net/PiggyXP/article/details/6922277?spm=1001.2014.3001.5502
Socket:https://blog.csdn.net/weixin_39634961/article/details/80236161
APM:https://www.cnblogs.com/sunev/archive/2012/08/07/2625688.html
EAP:https://www.cnblogs.com/tuyile006/p/10980391.html
EAP:https://segmentfault.com/a/1190000003834832?utm_source=tag-newest
SocketAsyncEventArgs:https://docs.microsoft.com/zh-cn/dotnet/api/system.net.sockets.socketasynceventargs?redirectedfrom=MSDN&view=net-6.0#code-snippet-1
SocketAsyncEventArgsPool:https://referencesource.microsoft.com/#System.ServiceModel/System/ServiceModel/Channels/SocketAsyncEventArgsPool.cs
Unicode:https://www.cnblogs.com/kingcat/archive/2012/10/16/2726334.html
ByteOrder: http://www.ruanyifeng.com/blog/2016/11/byte-order.html
MessageBoundary:https://www.cnblogs.com/kex1n/p/6502002.html