清晰易懂TCP通信原理解析(附demo、简易TCP通信库源码、解决沾包问题等)

目录

  • 说明
  • TCP与UDP通信的特点
  • TCP中的沾包现象
  • 自定义应用层协议
  • TCPLibrary通信库介绍
  • Demo演示
  • 未完成功能
  • 源码下载

说明

我前面博客中有多篇文章讲到了.NET中的网络编程,与TCP和UDP相关的有:

1.http://www.cnblogs.com/xiaozhi_5638/p/3167794.html

2.http://www.cnblogs.com/xiaozhi_5638/p/3169641.html

3.http://www.cnblogs.com/xiaozhi_5638/p/3290283.html

4.http://www.cnblogs.com/xiaozhi_5638/p/3313959.html

另外也有一些讲的是通过Socket模拟浏览器访问Web服务器,或者模拟Web服务器接收浏览器的请求:

1.http://www.cnblogs.com/xiaozhi_5638/p/3912668.html

2.http://www.cnblogs.com/xiaozhi_5638/p/3917943.html

(之前文章的排版不太好,不好意思!)

之所有对.NET中网络编程写得比较多,主要原因有两个,一是我公司做的项目多数跟通信这个有关;二是研究Socket通信工作模式有益于对软件架构设计的理解,因为它里面到处都使用到了“泵”结构,而这个结构几乎是所有框架、大型模块所必需具备的。另外,工作之余写的一本书(即将要出版)中有一章专门讲到了“泵”结构在软件系统中的作用。

这次写这篇文章主要是看了网上一个人提的有关TCP编程的问题,所以就再次整理了一下这方面的知识,并且做了一个“简易通信库”发出来给大家看看,代码很简单,功能也不是特别全,但是具备很好的扩展性,基本上可以用来说明.NET中TCP通信的工作模式。

TCPUDP通信的特点

关于对这两者的比较,网上一搜一大片,讲得也比较清楚。TCP通信就像打电话,双方通信之前需要建立连接、双方就位后方可开始会话;而UDP通信就像发短信,一方给另一方发送数据前,并不需要对方就位。

上面两幅图显示了TCP与UDP通信过程建立的区别。

除了它们通信过程建立的不同之外,两者还有以下区别:

  • TCP通信特点

1)可靠性

   通信双方均就位,一方发送数据,另一方收到后会做出回应,如果超时未发送成功,会自动重发,数据不会丢失。

2)顺序性;

   既然数据是按顺序走在建立的一条隧道中,那么数据遵循“先走先达到”的规则,并且隧道中的数据以“流”的形式传输,发送方发送的前后两次数据之间没有边界,需要接收方自己根据事先规定好的“协议”去判断数据边界。

3)高损耗。

   “高损耗”包括机器性能损耗高、宽带流量损耗高。因为通信双方时刻需要维持着连接的存在,这必然会损耗通信双方主机性能,要想维持隧道的通畅,通信双方必须不断地发送检测包和应答包,同时,它还支持数据重发等数据纠错功能,这些都将导致网络流量的增加。

  • UDP通信特点

1)不可靠性;

   既然无连接,发送方只管发送数据,而不管对方是否能够正确地接收到数据,更不负责数据超时重发等功能。

2)无序性;

   数据以“数据报”的形式发送,可以把“数据报”看成是一个“包”。如果把TCP传输数据比如成“河里的流水”,那么UDP传输数据就是‘邮局寄信’。发送方先发送的数据可能后到达,后发送的数据可能先到达,这个跟短消息类似。

3)低损耗。

   “低损耗”包括机器性能损耗低、宽带流量损耗低。UDP通信不需要维持一个连接的存在,所以它不需要消耗额外的机器性能。同时它也没有像TCP通信那样为了保持隧道的通畅,而必须不停地发送检测包和应答包,更不会进行一些数据检测纠错、重发等行为。

这次我们只讨论TCP通信。

TCP通信中的“沾包”现象

上面提到过,TCP通信中,数据是以“流”的形式传输的。前一次发送的数据和后一次发送的数据之间并没有明显的界限,这就会出现一个问题:当你收到一部分数据时,你无法判断接收到的数据是否是完整的?

如上图,发送方发送三次数据,而接收方可能一共分四次接收。并且每次接收到的数据量不确定(虽然每次收到的数据不确定,但是将四次接收到的数据拼接起来,与发送时的一致)。这样以来,当我们每次收到一份数据时,我们无法轻易判断(几乎不能)收到的数据是否完整(是否可以正确地被处理)。

以上现象我们称之为“沾包”。TCP通信过程中,要想解决“沾包”问题,我们必须人工采取一些措施,比如在发送数据时遵循一些“规则”,在接收到数据时,再按照相同的“规则”去解析数据,最终得到一份完整的数据,并进行正确的处理。没错,这里说的“规则”便是我们通常听到的“协议”。

关于协议,讲到的地方也很多。简单的说,协议就是一种“数据结构”,合作双方必须同时按照相同的数据结构发送/接收数据,比如传输层的TCP/UDP协议,又比如应用层的HTTP/FTP等协议。B/S结构系统使用到的协议见下图:

在TCP通信中,在发送和接收数据的时候,如果我们遵循事先定义的一种“协议”(属于一种应用层协议)。比如,在发送数据时,按照“数据头(4Byte)+内容长度(4Byte)+内容正文(NByte)+附加信息(8Byte)”这种形式去“格式化”需要发送的数据;同理,在接收到数据后,按照这种形式去“反格式化”数据,这样我们便可以判断数据边界,轻松得到一条完整数据。

自定义应用层协议

是的。我们自己完全可以定义一个类似HTTP这样的应用层协议,只要你能力足够强,系统足够大。今天在这里,我只举个简单的例子,假设一个TCP通信系统中,客户端连接上服务器后,客户端向服务器发送一个字符串,并发送一个字符串转换指令(比如大小写转换、除去特殊字符等指令),服务器接收到数据后,按照对应的指令,将字符串转换后发送回给客户端。那么这里的应用层协议可以这样设计:

字符串转换指令


序号


指令值(byte)


说明


1


0x01


将字符串中小写字符转换成大写


2


0x02


将字符串中大写字符转换成小写


3


0x03


去掉字符串中的百分号(%)字符


4


0x04


将字符串中的百分号(%)替换为空格

如上表所示,假设一共有四种字符串转换请求,那么我们可以按下面图设计应用层协议的数据结构:

如上图所示,开头一个字节代表字符串转换指令类型,后续四个字节存放一个Int32的整型数据,表示字符串的长度(字符串采用Unicode编码),最后N个字节表示字符串内容。数据发送方必须按照此协议格式发送数据,数据接收方必须按照此协议格式接收数据。

发送数据时按照协议格式化数据很简单,但是,接收数据后,按照协议去解析数据该怎样呢?事实上,这个相对来讲稍微复杂一点。我们可以将每次接收到的数据(字节流)写入一个缓冲区,然后判断缓冲区中是否存在一条完整的数据,如果存在,则处理这条完整的数据;否则,继续接收数据,将接收到的数据再次写入缓冲区...以此循环。

TCPLibrary通信库介绍

其实我只是将一些代码单独拿出来生成了一个dll,这部分代码可以为我们搭建起TCP通信的框架,包括服务端侦听、(服务端/客户端)接收数据、上下线、消息处理并通知Application以及“沾包”问题处理等等。功能并不全面,如果要拿去实际项目中使用还需要自己完善,文章末会列出未完成的功能。

TCP通信过程建立之后,大概结构如下:

整个通信库中,只包含5个抽象类,以及5个默认实现类(所以说简易):

使用该通信库的前提是要定义好程序使用到的“协议”,然后重点实现ZMessage.RawData属性和ZDataBuffer.TryReadMessage方法,前者可以按照协议格式化需要发送的数据,后者可以按照协议解析一条完整的消息。库中包含5个默认实现类(以Base开头的),它默认使用以下的协议进行通信:

其中,BaseDataBuffer.TryReadMessage方法具体实现为:

 1         /// <summary>
 2         /// 按照规定协议,重写TryReadMessage方法
 3         /// </summary>
 4         /// <returns></returns>
 5         internal override ZMessage TryReadMessage()
 6         {
 7             if (_length >= 8)   //  4 + 4 + N
 8             {
 9                 using (MemoryStream ms = new MemoryStream(_buffer))
10                 {
11                     BinaryReader br = new BinaryReader(ms);
12                     int msgtype = br.ReadInt32();  //读取消息类型
13                     int msglength = br.ReadInt32();  //读取消息长度
14                     if (_length - 8 >= msglength)  //如果缓冲区中存在一条完整消息,则读取
15                     {
16                         byte[] msgcontent = br.ReadBytes(msglength);  //读取消息内容
17                         BaseMessage bm = new BaseMessage(msgtype, msgcontent); //还原成一条完整的消息
18                         Remove(8 + msglength);  //注意! 移除已读数据
19
20                         return bm;  //返回读取到的消息
21                     }
22                     else
23                     {
24                         return null;
25                     }
26                 }
27             }
28             else
29             {
30                 return null;
31             }
32         }

BaseMessage.RawData属性具体的实现为:

 1         /// <summary>
 2         /// 按照规定协议,重写RawData属性
 3         /// </summary>
 4         public override byte[] RawData
 5         {
 6             get
 7             {
 8                 byte[] rawdata = new byte[4 + 4 + MsgContent.Length];  //消息类型 + 消息长度 + 消息内容
 9                 using (MemoryStream ms = new MemoryStream(rawdata))
10                 {
11                     BinaryWriter bw = new BinaryWriter(ms);
12                     bw.Write(MsgType);  //先写入MsgType
13                     bw.Write(MsgContent.Length);  //再写入MsgContent的长度
14                     bw.Write(MsgContent); //最后写入消息内容
15                     return rawdata;
16                 }
17             }
18         }

可以看到,上面一个按照协议格式化数据,而另一个按照协议解析数据。它们两个完全遵守同一个协议。

Demo演示

使用TCPLibrary中的默认实现类(以Base开头的类型),我做了一个简单的Demo。该Demo可以完成字符串、可序列化对象(图片)的发送与接收。Demo源码很简单:

l  Server端初始化:

1         private void Form1_Load(object sender, EventArgs e)
2         {
3             _server = new BaseServerSocket();
4             _server.Connected += new ConnectedEventHandler(_server_Connected);
5             _server.DisConnected += new DisConnectedEventHandler(_server_DisConnected);
6             _server.MessageReceived += new MessageReceivedEventHandler(_server_MessageReceived);
7             _server.StartAccept(9100);
8             textBox1.AppendText("服¤t务?器¡Â启?动¡¥,ê?监¨¤听¬y端?口¨² " + 9000 + "...\r\n");
9         }

l  Client端的初始化:

1         private void Form1_Load(object sender, EventArgs e)
2         {
3             _client = new BaseClientSocket();
4             _client.Connected += new ConnectedEventHandler(_client_Connected);
5             _client.DisConnected += new DisConnectedEventHandler(_client_DisConnected);
6             _client.MessageReceived += new MessageReceivedEventHandler(_client_MessageReceived);
7             _client.Connect("127.0.0.1",9100);
8         }

可以看到,使用起来很简单。注册事件后,既可以开始运行了。

下面可以看一下Demo截图:

注意,这个Demo只是利用库中的默认实现类来完成的。你完全可以自己定义一个协议,按照你自己的方式发送数据,比如“头(4Byte)+是否加密(1Byte)+发送方程序版本(8Byte)+消息长度(4Byte)+消息内容(NByte)+附加信息(8Byte)”这种方式发送数据/接收数据。只要你正确的实现了上面强调的方法和属性。

未完成功能

刚开始就说过,TCPLibrary功能不足,很多功能都没有。列举几个如下

1.线程安全

2.心跳检测

3.都只有开始,没有结束的功能

4.。。。

可以把源码下下来,自己尝试补充这些功能。

源码下载

下载地址:http://files.cnblogs.com/xiaozhi_5638/TCPDemo.rar

Win7+VS2010

希望有帮助!

时间: 2025-01-11 04:58:22

清晰易懂TCP通信原理解析(附demo、简易TCP通信库源码、解决沾包问题等)的相关文章

68:Scala并发编程原生线程Actor、Cass Class下的消息传递和偏函数实战解析及其在Spark中的应用源码解析

今天给大家带来的是王家林老师的scala编程讲座的第68讲:Scala并发编程原生线程Actor.Cass Class下的消息传递和偏函数实战解析 昨天讲了Actor的匿名Actor及消息传递,那么我们今天来看一下原生线程Actor及CassClass下的消息传递,让我们从代码出发: case class Person(name:String,age:Int)//定义cass Class class HelloActor extends Actor{//预定义一个Actor  def act()

【java项目实践】详解Ajax工作原理以及实现异步验证用户名是否存在+源码下载(java版)

一年前,从不知道Ajax是什么,伴随着不断的积累,到现在经常使用,逐渐有了深入的认识.今天,如果想开发一个更加人性化,友好,无刷新,交互性更强的网页,那您的目标一定是Ajax. 介绍 在详细讨论Ajax是什么之前,先让我们花一分钟了解一下Ajax做什么.如图所示: 如上图展示给我们的就是使用Ajax技术实现的效果.伴随着web应用的越来越强大而出现的是等待,等待服务器响应,等待浏览器刷新,等待请求返回和生成新的页面成为了程序员们的最最头疼的难题.随着Ajax的出现使web应用程序变得更完善,更友

co函数库源码解析

一.co函数是什么 co 函数库是著名程序员 TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行.短小精悍只有短短200余行,就可以免去手动编写Generator 函数执行器的麻烦 二.co函数怎么用 举个栗子就能清楚的知道如何使用co函数 1 function* gen(){ 2 var f1 = yield func1; 3 var f2 = yield fnuc2; 4 //sth to do 5 }; 手动执行和co函数执行的写法如下

Spark消息通信原理(一)——Spark消息通信架构

在Spark中定义了通信框架的接口,这些接口中调用了Netty的具体方法(在spark2.x前,使用的是Akka).各接口和实现类的关系如下图所示. 将终端(EndPoint)注册到Rpc环境中: 在各个模块中,如DriverEndPoint.ClientEndPoint.Master.Worker等,会先使用RpcEnv的静态方法创建RpcEnv实例,然后实例化终端,由于终端都是继承与ThreadSafeEpcEndPoint,即创建的终端实例属于线程安全的,接着调用RpcEnv的启动终端方法

iOS开源库源码解析之Mantle

来自Leo的原创博客,转载请著名出处 我的StackOverflow 这个源码解析系列的文章 AsnycDispalyKit SDWebImage Mantle(本文) AFNetworking(3.0) MBProgressHud SwiftJSON MagicRecord Alamofire 前言 iOS开发中,不管是哪种设计模式,Model层都是不可或缺的.而Model层的第三方库常用的库有以下几个 JSONModel Mantle MJExtension JSON data到对象的转换原

Android常用库源码解析

图片加载框架比较 共同优点 都对多级缓存.线程池.缓存算法做了处理 自适应程度高,根据系统性能初始化缓存配置.系统信息变更后动态调整策略.比如根据 CPU 核数确定最大并发数,根据可用内存确定内存缓存大小,网络状态变化时调整最大并发数等. 支持多种数据源支持多种数据源,网络.本地.资源.Assets 等 不同点 Picasso所能实现的功能,Glide都能做,无非是所需的设置不同.但是Picasso体积比起Glide小太多. Glide 不仅是一个图片缓存,它支持 Gif.WebP.缩略图.Gl

iOS开源库源码解析之SDWebImage

来自Leo的原创博客,转载请著名出处 我的stackoverflow 这个源码解析系列的文章 AsnycDispalyKit SDWebImage(本文) 前言 SDWebImage是iOS开发中十分流行的库,大多数的开发者在下载图片或者加载网络图片并且本地缓存的时候,都会用这个框架.这个框架相对来说,源代码还是比较少的.本文会详细的讲解这些类的架构关系和原理. 本文会先介绍类的整体架构关系,先有一个宏观的认识.然后讲解sd_setImageWithURL的加载逻辑,因为这是SDWebImage

iOS运营级B2B服务平台App、自定义图标库、个人中心页面、识别身份证Demo、瀑布流等源码

iOS精选源码 简单的个人中心页面-自定义导航栏并予以渐变动画 一个近乎完整的可识别中国身份证信息的Demo 可自动快速... iOS可自定义图表库 - PNChart 开源一款曾是运营级的B2B服务平台APP<云采> 使用ffmpeg解码最简iOS播放器 注释得非常清楚的瀑布流,和自己的一些想法 iOS日志框架学习分享 在iOS App中录制MP3和AMR:ZWAudioRecordTool 一套应用于swift项目的空白页组件EmptyPage 2.0 扫雷简单实现 iOS优质博客 iOS

利用jsoup解析电影天堂资源的应用程序包含源码

大家好!2014年的年尾,心血来潮利用一点点时间利用jsoup解析网页技术解析了“电影天堂”网站的视频资源.其中主要涉及到的技术有jsoup解 析,imageloader加载图片.android侧滑和简单的UI布局.但是有个缺陷是获取了所有的下载地址,但是没有实线下载的功能,有兴趣的朋友 可以接着实现该功能,或者利用手机迅雷进行下载. 下载地址为: http://pan.baidu.com/s/1hq1n6Oc#0-qzone-1-91781-d020d2d2a4e8d1a374a433f596