从TCP到铜线
应用层
应用层功能是应用程序(游戏程序)提供的功能。在给客户端发送“hello”的例子中,程序把“hello”转化成二进制流传递给传输层(传送给send方)。操作系统会对二进制数据做一系列加工,使它适合于网络传输。
传输层
收到二进制数据后,传输层协议会对它做一系列加工,并提供数据流传送、可靠性校验、流量控制等功能。
网络层
IP协议会给TCP数据添加本地地址、目的地地址等信息
数据传输流程
TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,与TCP相对应的UDP协议是无连接的、不可靠的协议,但传输效率比TCP高。
TCP连接的建立
在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过三次握手进行初始化的。三次握手的目的是同步连接双方的序列号和确认号并交换TCP窗口的大小信息。
连接方调用Connect后,Client(连接方)向Server(监听方)发送一个数据包SYN, SYN包含了序列号seq,这是以后传送数据时要使用的。Server收到数据包后由标志位SYN知道Client请求建立连接,Server将SYN/ACK数据包发送给Client以确认连接请求。Clients收到SYN/ACK数据包后Connect返回,连接成功
TCP的数据传输
发送一个数据后,发送方并不能确保数据被对方接收。于是发送方会等待接收方的回应,如果太长时间没有收到回应,发送方会重新发送数据。发送数据时,TCP会考虑对方缓冲区的容量,当对方缓冲区满时,会暂停发送数据,防止对端溢出。TCP还会根据数据返回的时间判断网络是否拥堵,如果网络拥堵就减慢发送的速度,以求“道路畅通”。
TCP连接终止
TCP通过“四次挥手”确保双端释放socket资源
- 第一次挥手:主机1(可以是客户端也可以是服务端)向主机2发送一个终止信号(FIN),此时,主机1进入FIN_WAIT_1状态,它没有需要发送的数据,等待着主机2的回应。
- 第二次挥手:主机2收到了主机1发送的终止信号(FIN),向主机1回应一个ACK。收到ACK的主机1进入FIN_WAIT_2状态。
- 第三次挥手:在主机2把所有数据发送完毕后,主机2向主机1发送终止信号(FIN),请求关闭连接。
- 第四次挥手:主机1收到主机2发送的终止信号(FIN),向主机2回应ACK。然后主机1进入TIME_WAIT状态(等待一段时间,以便处理主机2的重发数据)。主机2收到主机1的回应后,关闭连接。至此,TCP的四次挥手便完成了,主机1和主机2都关闭了连接
常用TCP参数
ReceiveBufferSize
ReceiveBufferSize指定了操作系统读缓冲区的大小,默认值是8192,可以通过 socket. ReceiveBufferSize = 8 这样来指定缓冲区的长度
SendBufferSize
SendBufferSize指定了操作系统写缓冲区的大小,默认值也是8192
NoDelay
指定发送数据时是否使用Nagle算法,对于实时性要求高的游戏,该值需要设置成false。Nagle是一种节省网络流量的机制,默认情况下,TCP会使用Nagle算法去发送数据。
TTL
TTL指发送的IP数据包的生存时间值(Time To Live, TTL)。TTL是IP头部的一个值,该值表示一个IP数据报能够经过的最大的路由器跳数。发送数据时,TTL默认为64
在网络游戏中,如果某些偏远地区用户时不时无法接收数据,可以尝试增大TTL值(socket.ttl=xxx)来解决问题。
ReuseAddress
ReuseAddress即端口复用,让同一个端口可被多个socket使用。一般情况下,一个端口只能由一个进程独占,假设服务端程序都绑定了1234端口,若开启两个服务端程序,虽然,第一个开启的程序能够成功绑定端口并监听,但第二个程序会提示“端口已经在使用中”,无法绑定端口。
当服务端程序崩溃,但它持有的Socket不会被立马释放,这时候重启服务器就会遇到“端口已经在使用中”的情形。等到Socket被释放后(这个过程可能要十几分钟时间),服务端才能成功重启。
设置端口复用使用socket的SetSocketOption方法,代码如下所示。
Socket socket= new Socket(AddressFamily.InterNetwork, SocketType.Stream,
ProtocolType.Tcp);
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress,
true);
LingerState
LingerState的功能是设置套接字保持连接的时间
客户端调用Close()关闭Socket连接(客户端或服务端关闭连接都是同样的流程,服务端主动关闭连接同理),这时,客户端会给服务端发送FIN信号(①),然后进入等待。当服务端收到FIN信号时,会返回一个长度为0的数据,然后向客户端回应信息(②)。这也是为什么关闭连接时,对端Receive会收到0个数据。如果服务端不做处理,客户端将会持续等待。
服务端中,会使用下面的代码处理客户端主动关闭连接,即在收到长度为0的消息后,调用clientfd.Close()关闭连接。
public static void ReceiveCallback(IAsyncResult ar){
……
int count = clientfd.EndReceive(ar);
//客户端关闭
if(count == 0){
clientfd.Close();
……
return;
}
……
}
服务端在调用Close后,它向客户端发送FIN信号(③),然后等待客户端回应。当服务端收到客户端的回应信息时,它会释放socket资源,真正完成关闭连接的流程。对客户端来说,它在收到服务端的FIN(③)信号后,会进入一个称为TIME_WAIT的状态,等待一段时间后(Windows下默认为4分钟),才会释放socket资源,真正完成关闭连接的流程。TIME_WAIT状态的意义在于,如果网络状况不好,服务端迟迟没有收到客户端回应的信号(④),那它会重发FIN信号(③),客户端socket需要维持一段时间,以回应重发的信号,确保对方有很大概率能够收到回应信号(④)。
这种机制可以让服务端在关闭连接前处理尚未完成的事情,例如,假设收到客户端FIN信号时,服务端socket处于图片显示的状态,即发送缓冲区还有尚未发送的数据,那么直接调用Close关闭连接,缓冲区中的数据将被丢弃。这种关闭方式很暴力,因为对端可能还需要这些数据
socket.LingerState = new LingerOption (true, 10);
其中的LingerOption带有两个参数。第一个参数是LingerState.Enabled,代表是否启用LingerState,只有设置为true才能生效。第二个参数是LingerState.LingerTime,指定超时时间。如果超时时间大于0(比如10秒),操作系统会尝试发送缓冲区中的数据,但如果网络状况不好,超过10秒还没有发完,它还是会强制关闭连接。如果LingerState.LingerTime设置为0,系统会一直等到数据发完才关闭连接,无论等待多长时间。开启LingerOption能够在一定程度上保证发送数据的完整性
心跳机制
TCP有一个连接检测机制,就是如果在指定的时间内没有数据传送,会给对端发送一个信号(通过SetSocketOption的KeepAlive选项开启)。对端如果收到这个信号,回送一个TCP的信号,确认已经收到,这样就知道此连接通畅。如果一段时间没有收到对方的响应,会进行重试,重试几次后,会认为网络不通,关闭socket。
Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive,
true)