对于TCP或者UDP来说,它们作于传输层的协议,有着自己的标准,或者叫格式,在我们看TCP格式之前先了解一下计算机的基础知识,字节,它是计算机世界的一个小单位,也是我们可以理会到的,如一个utf-8英文字母表示一个字节(byte),而一个汉字或者一个unicode的英文字母则表示为两个字节,或者说在计算机中它将占两个字节的存储空间。而在编程世界里,通常所说的Int32,Int16,Int64这些是类型的后缀是指它占用的位数(bit,8bit=1byte),即一个int32类型的数字,占用4个字节的存储空间,其实这在之前的教课书上大家都见过,也都背过,但可能就是没有用过,学以不至用,不是一件好事,下面就让大家学以至用一下。
TCP协议格式
TCP各字段的含义
Source Port & Destination Port
如果我们将IP比喻成地址那麽Port可以说是门口了试想一下一座大楼有前门後门侧门送货的门出货的门倒垃圾的门扔死尸的门等等乱七八糟的门...... 那麽一个IP地址也有着好多个各种功能的port而每一个port都被不同的服务倾听着就好比看门人一样。下面是一些常用port和其对应的服务有兴趣的朋友可以在Linux的/etc/services这个档案找到它们
ftp-data 20/tcp
ftp 21/tcp
telnet 23/tcp
smtp 25/tcp mail
www 80/tcp http # WorldWideWeb HTTP
www 80/udp # HyperText Transfer Protocol
pop-3 110/tcp # POP version 3
pop-3 110/udp
其实port号码可以随您喜欢任意指定给哪些服务使用但为了避免“找错门口”的情形出现(除非您故意想躲起来)人们将一些比较常用的服务(Well known services)的port号码固定下来了。但是在TCP资料传送过程中可能同时要处理一个以上的封包程式也会建立多个port来避免突。在两台主机进行资料传送的时候来源地的port和目的地的port都必须让TCP知道才行。
Sequence Number
发送序号。当资料要从一台主机传送去另一台主机的时候发送端会为封包建立起一个初始号码然後按照所传送的位元组数依次的递增上去那麽下一个封包的序号就会使用递增之後的值来作为它的序号了。这样接收端就可以根据序号来检测资料是否接收完整了。
Acknowledgement Number
回应序号。当接收端接收到TCP封包之後通过检验确认之後然後会依照发送序号产生一个回应序号发出一个回应封包给发送端这样接收端就知道刚才的封包已经被成功接收到了。
可是如果由於网路状况或其它原因当封包的TTL值达到期限时接收端还没接收到回应序号就会重发该个被以为丢失了的封包。但如果刚好重发封包之後才接收到回应呢这时候接收端就会根据序号来判断该封包是否被重发送如果是的话很简单将之丢弃不做任何处理就是了。
Data Offset
这是用来记录标头固定长度用的,和IP封包的IHL差不多。如果options没设定的话,其长度就是20byte,用十六进位表示就是 0x14了。
Reserved
这是保留区间暂时还没被使用。
Contral Flag
控制标记。一共有六个它们分别是: Urgent data
如果URG为1,表示这是一个携有紧急资料的封包。
Acknowledgment field significant
如果ACK为1,表示此封包属于一个要回应的封包。一般都会为1。
Push function
如果PSH为1,此封包所携带的数据会直接上传给上层应用程序而无需经过TCP处理。
Reset
如果RST为1,要求重传。表示要求重新设定封包再重新传递。
Synchronize sequence number
如果SYN为1,表示要求双方进行同步沟通。
No more data for sender (Finish)
如果FIN为1,表示传送结束,然後双方发出结束回应进而正式终止一个TCP传送过程。
Window
我们都知道MS Windows是什么东西,但这里的Window却非操作系统的“视窗”哦,这里一般称为“滑动视窗(Sliding Window)”。为什么我们需要使用视窗呢?
正如您刚才看到的TCP封包会通过SQN和ACK序号来确保传送的正确性,但如果每一个封包都要等上一个封包的回应才被发送出去的话实在是太慢和难以接受的。这样我们可以利用Sliding Window在传送两端划分出一个范围,规定出可以一次性发送的最大封包数目。
当TCP传送建立起来之後两端都会将window的设定值还原到初始值比方说每次传送3个封包。然后发送端就一次发送三个封包出去,然后视窗则会往後移动三个封包填补发送出去之封包的空缺。如果接收端够顺利也能一次处理接收下来的三个封包的话,就会告诉发送端的window值为3,但如果接收端太忙或是其它因素影响暂时只能处理两个封包,那么在视窗里面就剩下一个封包,然后就会告诉发送端window值为2。这个时候发送端就只送出两个封包而视窗就会往後移动两个封包填补发送出去的空缺。您明白为什麽这个视窗会“滑动”了吧。
其实,Window值是以字节数计算的。
Chechsum
当资料要传送出去的时候发送端会计算好封包资料大小然後得出这个检验值封包一起发送当接收端收到封包之後会再对资料大小进行计算看看是否和检验值一致如果结果不相称则被视为残缺封包会要求对方重发该个封包。
Urgent Pointer
还记得刚才讲到Control Flag的时候我们提到一个URG的标记吗如果URG被设定为一的时候这里就会指示出紧急资料所在位置。不过这种情形非常少见例如当资料流量超出频宽的时候系统要求网路主机暂缓发送资料所有主机收到这样的信息都需要优先处理。
Option
这个选项也比较少用。当那些需要使用同步动作的程式如Telnet要处理好终端的交互模式就会使用到option来指定资料封包的大小因为telnet使用的资料封包都很少但又需要即时回应。
Option的长度为0,或32bit的整倍数,如果不足则填充到满。
自己动手,搞自己的协议格式
[bodyLength int32][id int32][type int16][name char(20)][body byte[]]
下面我们来测试一个协议,为它赋值,模拟一下client和server的发送与接受的过程
string name = "zzl"; var message = SerializeHelper.SerializeToBinary(new UserDTO { ID = 1, Name = "repositoryUncle" }); int bodyLength = 4 + 4 + 2 + name.Length + message.Length; byte[] buffer = new byte[bodyLength]; byte[] body = new byte[bodyLength]; #region (发送端)写入字节流 //write message length Buffer.BlockCopy(BitConverter.GetBytes(bodyLength), 0, buffer, 0, 4); //write id. Buffer.BlockCopy(BitConverter.GetBytes(101), 0, buffer, 4, 4); //write response type. Buffer.BlockCopy(BitConverter.GetBytes((short)4), 0, buffer, 8, 2); //write response name. Buffer.BlockCopy(Encoding.ASCII.GetBytes(name), 0, buffer, 10, name.Length); //write response message. Buffer.BlockCopy(message, 0, buffer, 10 + name.Length, message.Length); #endregion #region (接收端)读字节流 var messageLength = BitConverter.ToInt32(buffer, 0); var id = BitConverter.ToInt32(buffer, 4); var type = BitConverter.ToInt16(buffer, 8); var nameVal = Encoding.ASCII.GetString(buffer, 10, name.Length); var dataLength = messageLength - 10 - name.Length; Buffer.BlockCopy(buffer, 10 + name.Length, body, 0, dataLength); var obj = (UserDTO)SerializeHelper.DeserializeFromBinary(body); #endregion
我们在测试时,可以设置断点去观察一下messageLength,id,type,nameVal,obj等元素的值,是否正确解析了,呵呵。
最后一点要记住,协议并不难,字节流也并不神秘,关键在于坚持,不断的去探索、研究,有了这几点,再麻烦的事赞也能搞定!