基于TCP流协议的数据包通讯

时间:2022-02-21 15:39:13

                                                                                                                     Fanxiushu   2016-02-04,引用或转载请注明原始作者.

TCP通讯是流协议,它不像UDP那样基于包为边界的通讯方式,

TCP流式协议,举个简单例子,一端用send 分别发送 100,123,120字节的数据,
另一端用recv可以一下子接收到 100+123+120=343字节的数据,或者先接收 3个字节的数据,再接收余下的340字节,
不管另一端怎么接收,最终是要接收到343字节的数据。
而且TCP保证数据的完整性和顺序,也就是两端是数据同步的,出现任何一点的数据不一致,都会造成TCP连接的失效。
UDP则跟TCP大不一样,他是基于包边界的。所谓的包边界,就是一端分别发送 100, 123, 120字节的数据,
另一端接收到也应该分别是 100,123,120字节数据的三个包,
不会出现一端发送100字节的一个数据包,另一端只接收到小于100字节的数据包,或者收到大于100字节的数据包。
UDP同时也不保证稳定和顺序,如发送端发送100,123,120三个包,接收端可能接收到3个包,也可能只接收到2个包,
也可能一个包也收不到,收到的顺序不一定是100,123,120,可能是100,120,123,或者123,100,120等。
这些TCP和UDP的属性,大家稍微查查资料就该很清楚。
UDP的这种特殊通讯方式其实跟网络底层链路层的通讯方式很接近。
链路层的数据是一个数据包一个数据包的传输,并不保证数据能否达到对方,或者按照顺序到达对方。
UDP只是简单把链路层和IP层的数据加了一层封装,加了端口用于识别同一个机器的不同进程,
UDP数据包的收发方式,只是组合成UDP包之后,简单的发送到底层网络了事,
至于底层网卡有没有发成功或者接收成功,它是一概不闻不问的。
他的底层处理方式比起 TCP协议来说简单太多了。

正是因为UDP的简单和直接,所以在某些场合他是非常实用的。
比如定时报告状态,只需要很少数据量的包,也不必担心包丢失问题,反正都是定时发送。
再比如某些游戏,尤其是实时对战游戏,UDP简直可以说是为他们量身定制的。
因为这类注重即时性的游戏,对延迟要求比较高,使用UDP收发少量的对战类数据包,简直是最佳的选择。
UDP还有一个好处,就是编程的IO模型简单,
在服务端你可以简单到开启一个读线程和一个写线程,就能接收和发送数据到所有的客户端。
而TCP的IO模型往往要复杂得多。

既然UDP这么好,编程又简单,可现在网络中大部分都在使用TCP,
一个非常重要的原因就是TCP提供的是可靠传输,TCP有一套复杂的底层算法来保证数据的完整和可靠,
有这个理由就已经足够让TCP在大部分场所比UDP好使了。
因为大部分时候,我们在开发网络通信程序,都希望能随意的接收和发送任意大小和完整的数据,
如果使用UDP,还得自己写算法来保证数据的顺序和完整,整个处理过程就等于实现一个小型的TCP协议。
一些特殊场所,比如P2P,各种使用P2P的下载软件如迅雷等,
这些软件和传统的服务端客户端模式不大一样,每个运行软件的机器既是客户端也是服务端,
而用户的每个机器可能处于不同的网络环境中,最典型的就是大部分机器处于NAT中,
这样的环境下,采用UDP是最佳选择,因为TCP的NAT穿透能力差。
当然这些软件使用UDP,他们也必须实现一套算法来保证UDP传输的完整和顺序。

我们在开发TCP程序时候,最先想到的就是 请求-应答模式:
就是客户端发起一个请求,然后服务端接收到请求,进行处理,接着向客户端应答这个请求。
最典型和常用的就是 HTTP协议,我们浏览的所有网页,以及各种玲琅满目的网站,
这些都是HTTP的功劳,HTTP协议是建立在TCP上的应用层协议,采用就是 请求-应答方式。
浏览器首先发起一个网页请求的TCP连接,web服务器通过这个TCP连接应答这个网页,并把网页内容传输给浏览器。
然后浏览器可能关闭这个TCP连接,或者也可能利用这个TCP连接发起另外一个网页请求。
这个请求-应答模式,也是我在使用TCP开发私有协议时候,使用的最多的模式,
多得来以至于都忘记其他模式需求的存在了。

现在我们来看另一个通讯情况:windows远程桌面。
使用远程桌面可以远程控制另一台windows机器,可以在远程桌面里做任何本地桌面上的操作,
比如删除,复制文件,可以把本地文件复制到远程机器里,在复制的同时还能执行其他操作,
远程机器的桌面变化实时更新到本地,等等。
但是仔细研究会发现,远程桌面只使用了一条 TCP连接,连接到被控制机器的 3389 端口。
也就是在一条TCP通讯连接里,传输各种请求数据和接收各种应答数据。
远程桌面使用的是 RDP协议,我们这里不讨论RDP的细节,
只讨论如何在一条TCP连接中,如何做到远程桌面的各种操作。
如果我们还是按照请求-应答的模式来解释远程桌面的通讯协议,显然会有很多无法处理的问题。
比如举个简单例子:
我们在远程桌面客户端点击鼠标操作,这个操作会通过3389的TCP连接发送到被控制端,如果按照请求-应答模式来工作,
则必须在被控制端接收到这个鼠标操作,执行这个动作,然后回答给客户端已经执行了这个操作。
如果这期间,被控制机器的桌面界面内容发生变化,则无法通知给客户端,
因为一切通讯都是按照客户端发起请求,然后服务端应答的方式通讯的。
即使我们使用请求-应答的方式,通过轮询定时查询被控制机器的界面内容变化情况,也无法做到实时,
而且轮询慢了会严重影响视觉效果,轮询快了会严重浪费资源。

于是,我们改换一种解决问题的办法,从 UDP 通讯的特点:(按照包模式通讯)入手去解决上边的问题。
假定我们在远程桌面的TCP通讯中,一切通讯的数据都定义成一个一个的单独的数据包在同一条TCP连接中传输,
数据包的接收和发送分开进行,就是在同一个TCP连接中,一个线程专门接收数据包,一个线程专门发送数据包。
这是可以的,因为现在的网卡都是工作在全双工状态下。所谓全双工,就是接收和发送使用各自的通道,能独立进行数据传输。
大致伪代码如下:

int tcp_socket = 客户端连接到服务端的socket或者服务端接收到客户端连接的socket。
receive_thread() //负责接收数据包的线程
{
      tcp_packet = recv_packet (tcp_socket );
      ////TCP 是流协议,因此,我们必须至少定义一个表示包大小的头+包内容,才能保证TCP数据传输的同步。
     
       //处理 tcp_packet 包,为了不阻塞读线程,一般是把tcp_packet交给别的线程处理。
}
send_thread()//负责发送数据包的线程
{
     while(loop){
          从发送队列取出一个包 tcp_packet,(发送队列,是别的线程生成的需要发送的数据包。)
          send_packet( tcp_socket, tcp_packet ); //发送这个数据包。
     }
}

再回到上边的问题,
远程桌面控制端(客户端)和被控制端(服务端),分别开启两个线程,一个负责接收数据包,一个负责发送数据包。
当我们在远程桌面客户端点击鼠标等操作,生成一个鼠标的数据包投递到发送线程,
发送线程再把它传输到被控制端,被控制端接收到这个数据包,然后执行,
他如果要回复这个鼠标的执行结果,则再生成一个结果包投递到发送线程,发送线程再把这个包传输给客户端。
同时如果被控制端的界面发生改变,则生成一个界面内容改变的数据包,投递到发送线程,发送线程再传输给客户端。
客户端的接收线程接收到界面内容改变的数据包,显示新的被控制端的界面内容。
客户端接收到鼠标执行结果的包,知道鼠标操作是失败还是成功。
按照包的方式通讯,就能在远程桌面中传递各种复杂的动作,每个动作都封装成一个一个的数据包进行传输。
接收包和发送包分开独立进行,互相不干扰,每个包是否需要应答包,根据每个包的需求决定,不是必须的。
这又回到了 UDP通讯方式。那为何不干脆使用UDP代替呢?
还是上边提到的原因,TCP保证稳定和顺序,这点在远程桌面等这类要求数据必须准确的地方,是十分必要的。

再看看即时通讯,大家最熟悉的莫过于QQ了,还有已经进入历史的MSN。
QQ的通讯是混合模式,UDP和TCP都在使用,任何的通讯软件,UDP和TCP的混合使用有时是不可避免的。
即时通讯软件发送接收的聊天message,本身就是一个个的消息包,定义成数据包在TCP中传输,最合适不过了。
我们只讨论TCP连接的服务器转发聊天消息的情况。
假设用户A和B聊天。A只有一条TCP连接到服务端,同样B也只有一条TCP连接到服务端。
按照TCP的包方式进行通讯,
当A生成一条message,客户端程序组合成一个聊天包,发送到服务器端, 服务端找到这个包需要转发的B用户,
然后把这个包投递到B的发送线程队列,发送队列把数据包发送给B 。
B的接收线程接收到这个包,显示出来,于是B用户就看到A发来的消息,同时B也可能在发消息给A,
同样的道理,A也会收到B的消息。在A和B聊天期间,有些状态信息,比如 对方是否掉线,对方是否正在打字等等,
这些信息都组合成一个一个的数据包,在服务器和客户端之间接收和发送。
试想下,如果采用请求-应答的方式,根本无法处理即时聊天通讯。

我们再看看网络游戏通讯,各种大型的还是小型的,只要牵涉到网络通信的,
当然比较简单的只用HTTP通讯的游戏除外。
 游戏通讯中,基于数据包的方式是非常普遍的做法。

TCP是流协议,要怎样做,才能保证传输的是一个一个的单独的数据包,并且不破坏客户端和服务端之间的TCP连接的同步性呢?
其实是挺简单的:每个数据包定义成 ”包大小+包内容“,比如4个字节表示包的大小,然后是包数据。
发送的时候,“包大小+包内容”组合到一起发送,接收的时候,先接收固定的4个字节,获取到包的size,
然后再接收size字节的数据,这样一个包就接收完成。大致伪代码如下:
send_packet(tcp_socket, packet, packet_size) //发送数据包
{
     int32 size = pakcet_size; ///应该采用网络序
     send(tcp_socket, &size, 4..);
     send(tcp_socket, packet, packet_size);
}
recv_packet(tcp_socket)
{
       int32 size;
       recv(tcp_socket, &size, 4,...);
      char* packet = malloc(size);
      recv( tcp_socket, packet, size, ...);
     return packet;
}

当然,实际的通讯协议中,不会这么简单。是根据需求来定义包格式。

CSDN上的链接,提供的是本人实现的TCP按照包模式通讯的服务端框架代码。

框架来源于做的一个手游服务端项目,可惜项目没完成就中断了。
当然有很多现成的游戏通讯框架可以借用,当初坚持自己实现,是习惯了自己造*。

实现起来也不算难,而且出了问题更容易调试BUG 。


框架实现了可以同时侦听多个端口,
每个数据包既可以不压缩传输,也能支持zlib压缩和blowfish加密传输。
服务端提供三种线程池来进行tcp连接处理,
一类是接收线程池,接收线程池获取每个socket传输来的数据包,
同时保证每个socket的包按照到来的顺序进行处理,
二类是工作线程池,由接收线程池把接收到的数据包投递到工作线程池,
工作线程池专门处理这些接收到的数据包。
三类是发送线程池,当工作线程池处理完这些数据包,确定需要发送处理结果数据包到客户端,
或者其他线程需要发送数据包到客户端,他们首先把数据包投递到发送线程池,
发送线程池专门负责数据包的发送。
框架同时提供了每个客户端的定时器功能,在服务端内部各个socket之间数据通信等。
框架支持 Linux和windows平台。


代码CSDN下载地址:

http://download.csdn.net/detail/fanxiushu/9427108