您的位置:首页 > 理论基础 > 计算机网络

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

2016-02-03 23:43 363 查看
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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: