TCP协议
网络传输不稳定,需要进行多次编码和校验来确保数据的有效传输。
应用层
应用程序,游戏程序。
传输层
传输层协议收到二进制数据后,进行一系列加工,并提供数据流传送、可靠性检验、流量控制等功能。 确保对方收到信件的规则: (1)确认机制,收到信件的人必须回信,告诉对方收到了信件。 (2)信件寄出后,寄信人会等待回信。未收到回信,重新寄出。三次未回应,放弃。 TCP传输模型,层层加工数据。网络层限制,每个包最大数据量是65535字节。 TCP在网络层(IP协议)基础上,增加数据拆分(TCP数据包拆分成多个IP包)、确认重传、流量控制等功能。 TCP头部信息20个字节,一个TCP包的用户数据最多65535-20=65515个字节。
网络层
网络通信,数据包经过层层传送,最终到达目的地。IP协议会给TCP数据添加本地地址、目的地地址等信息。
网络接口
多层处理后,数据通过物理介质(电缆、光纤等)传输,IP协议还被封装成更为底层的链路层协议,已完成数据校验等功能。
数据传输流程
TCP,面向连接的,可靠的,基于字节流的传输层通信协议。 UDP,无连接到,不可靠的,传输效率高的协议。
TCP连接的建立
三次握手进行连接的初始化,目的是同步连接双方的序列号和确认号并交换TCP窗口的大小信息。 连接方调用Connect(SYN_SENT)后,Client(连接方)向Server(监听方)发送一个数据包SYN(包含序列号seq=x,用于传送数据) LISTEN(listen())状态的Server收到数据后由标志位SYN知道Client请求建立连接(SYN_RCVD),Server将ACK(x+1)/SYN(seq=y)数据包发送给Client以确认连接请求。 Client收到ACK(x+1)/SYN(seq=y)数据包后返回,连接成功,连接状态设置为ESTABLISHED。 之后Client发送ACK(y+1),Server收到ACK包后,连接状态设置为ESTABLISHED,成功建立连接。
TCP数据传输
发送数据后,发送方等待对方回应,若太长时间未收到回应,重新发送数据。 对方缓冲区满时,暂停发送数据,防止对端溢出。 TCP会根据数据返回时间判断网络是否拥挤,若拥挤,减慢发送速度。
TCP连接终止
四次挥手确保双端释放socket资源。
- 第一次挥手:
主机1向主机2发送一个终止信号(FIN,close(write),主机1不再发送数据),[FIN seq=x+2 ACK=y+1],主机1进入FIN_WAIT_1状态,等待主机2的回应。 - 第二次挥手:
主机2收到主机1发送的终止信号(FIN),主机2进入CLOSE_WAIT状态(已接收所有数据,close(read))。主机2向主机1回应一个ACK[ACK x+3],收到ACK的主机进入FIN_WAIT_2状态。 - 第三次挥手:
主机2把所有数据发送完毕后,主机2进入CLOSE_WAIT状态(close(write))。主机2向主机1发送终止信号(FIN)[FIN seq=y+1],请求关闭连接,进入LAST_ACK状态。 - 第四次挥手:
主机1收到主机2发送的终止信号(FIN),向主机1回应ACK[ACK=y+2]。然后主机1进入TIME_WAIT状态(等待一段时间,以便处理主机2的重发数据)。主机2收到主机1的回应后,关闭连接。
常用TCP参数
ReceiveBufferSize
读缓冲区大小,默认8192,socket.ReceiveBufferSize=1024设置。
SendBufferSize
写缓冲区大小,默认8192,socket.SendBufferSize=1024设置。
NoDelay
发送数据是否使用Nagle算法,实时性要求高的游戏,需要设置socket.NoDelay=true。 Nagle算法机制,若发送端多次发送少量字节数据包,会积攒到一定数量组成较大数据包后再发送,可以提升网络传输效率,但会降低网络实时性。
TTL
IP数据包的生存时间值(Time To Live,TTL),IP头部值,IP数据报能够经过的最大路由器跳数。 每经过一个路由器,值减一,为零时丢弃数据,避免IP包在网络中的无限循环和收发。 Windows Xp默认128,Windows7默认64,Windows10默认65,Linux默认255。
ReuseAddress
端口复用,让同一个端口可被多个socket使用。 退出端口和释放端口并不同步,服务端程序崩溃,但它持有的Socket不会立即释放(十几分钟),端口无法使用。 端口复用,用于新启动服务器进程可以直接绑定端口,防止之前绑定的端口还未释放或程序突然退出而系统没有释放端口。
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType,Stream, ProtocolType.Tcp);
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
LingerState
设置套接字保持连接的时间。 TIME_WAIT状态下,等待一段时间(Windows默认4分钟),才释放socket资源,真正完成关闭连接的流程。 TIME_WAIT状态意义,维持一段时间,确保对端大概率收到FIN信号的回应。 这种机制可以让服务端关闭连接前处理尚未完成的事情。比如,服务端收到客户端FIN信号时,发送缓冲区还有数据,可以发送完数据后再关闭。
socket.LingerState = new LingerOption(true, 10);
Close的恰当时间
bool isClosing = false;
public void Close() {
if(writeQueue.Count>0) {
isClosing = true;
} else {
socket.Close();
}
}
public void Send() {
if(isClosing) {
return;
}
byte[] sendBytes = "发送数据";
ByteArray ba = new ByteArray(sendBytes);
writeQueue.Enqueue(ba);
if(writeQueue.Count==1){
socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0, SendCallback, sock);
}
}
public void SendCallback(IAsyncResult ar){
Socket socket = (Socket) ar.AsyncState;
int count = socket.EndSend(ar);
ByteArray ba = writeQueue.First();
ba.readIdx += count;
if(count==ba.length){
writeQueue.Dequeue();
if(writeQueue.Count==0&&isClosing){
socket.Close();
}
}else{
socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0, SendCallback, sock);
}
}
异常处理
Socket API抛出异常
EndReceive可能引发异常 | 发生条件 |
---|
ArgumentNullException | asyncResult为null | ArgumentException | asyncResult通过调用未返回BeginReceive方法 | InvalidOperationException | EndReceive之前已调用为异步读取 | SocketException | 尝试访问套接字时出错 | ObjectDisposedExcept | Socket已关闭 |
心跳机制
TCP连接检测机制,若指定时间内无数据传送,会给对端发送一个信号(开启SetSocketOption的KeepAlive选项,默认2小时)。 对端若收到这个信号,回送一个TCP信号,确认收到。 若一段时间未收到响应,重试,几次失败后,认为网络不通,关闭socket。
心跳机制指客户端定时(比如间隔1分钟)向服务端发送PING消息,服务端收到后回应PONG消息。 服务端会记录最后一次收到PING消息的时间,若两次收到PING时间间隔长(比如3分钟),就假定连接不通,关闭连接,释放系统资源。
心跳机制缺点:
- 短暂故障期间,释放良好连接;
- PING和PONG消息暂用不必要的带宽;
- 花费更多流量。
完整代码
ByteArray.cs
using System;
public class ByteArray {
const int DEFAULT_SIZE = 1024;
public byte[] bytes;
private int capacity = 0;
public int readIdx = 0;
public int writeIdx = 0;
public int remain { get { return capacity-writeIdx; }}
public int length { get { return writeIdx-readIdx; }}
public ByteArray(int size = DEFAULT_SIZE){
bytes = new byte[size];
capacity = size;
readIdx = 0;
writeIdx = 0;
}
public ByteArray(byte[] defaultBytes){
bytes = defaultBytes;
capacity = defaultBytes.Length;
readIdx = 0;
writeIdx = defaultBytes.Length;
}
public void ReSize(int size){
if(size < capacity)
return;
capacity = 1;
while(capacity<size)
capacity *= 2;
byte[] newBytes = new byte[capacity];
Array.Copy(bytes, readIdx, newBytes, 0, length);
bytes = newBytes;
writeIdx = length;
readIdx = 0;
}
public int Write(byte[] bs, int offset, int count){
if(remain < count){
ReSize(length + count);
}
Array.Copy(bs, offset, bytes, writeIdx, count);
writeIdx += count;
return count;
}
public int Read(byte[] bs, int offset, int count){
count = Math.Min(count, length);
Array.Copy(bytes, readIdx, bs, offset, count);
readIdx += count;
CheckAndMoveBytes();
return count;
}
public void CheckAndMoveBytes(){
if(length < 8){
MoveBytes();
}
}
public void MoveBytes(){
if(length>0) {
Array.Copy(bytes, readIdx, bytes, 0, length);
}
writeIdx = length;
readIdx = 0;
}
public Int16 ReadInt16(){
if(length < 2)
return 0;
Int16 ret = BitConverter.ToInt16(bytes, readIdx);
readIdx += 2;
CheckAndMoveBytes();
return ret;
}
public Int32 ReadInt32(){
if(length < 4)
return 0;
Int32 ret = BitConverter.ToInt32(bytes, readIdx);
readIdx += 4;
CheckAndMoveBytes();
return ret;
}
public override string ToString(){
return BitConverter.ToString(bytes, readIdx, length);
}
public string Debug(){
return string.Format("readIdx({0}) writeIdx({1}) bytes({2})",
readIdx,
writeIdx,
BitConverter.ToString(bytes, 0, capacity)
);
}
}
csc ByteArray.cs -t:library
Echo.cs
using System.Collections.Generic;
using System.Net.Sockets;
using System;
using System.Linq;
public class Echo {
static Socket socket;
static ByteArray readBuff = new ByteArray();
static Queue<ByteArray> writeQueue = new Queue<ByteArray>();
public static void Connect() {
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket);
}
public static void ConnectCallback(IAsyncResult ar) {
try {
Socket socket = (Socket)ar.AsyncState;
socket.EndConnect(ar);
socket.BeginReceive(readBuff.bytes, readBuff.writeIdx, readBuff.remain, 0, ReceiveCallback, socket);
} catch(SocketException ex) {
Console.WriteLine("Socket Connect Fail" + ex.ToString());
}
}
public static void ReceiveCallback(IAsyncResult ar){
try {
Socket socket = (Socket) ar.AsyncState;
int count = socket.EndReceive(ar);
readBuff.writeIdx += count;
OnReceiveData();
if(readBuff.remain < 8){
readBuff.ReSize(readBuff.length*2);
}
socket.BeginReceive(readBuff.bytes, readBuff.writeIdx, readBuff.remain, 0, ReceiveCallback, socket);
}
catch (SocketException ex){
Console.WriteLine("Socket Receive fail" + ex.ToString());
}
}
public static void OnReceiveData(){
if(readBuff.length <= 2)
return;
Int16 bodyLength = BitConverter.ToInt16(readBuff.bytes, 0);
if(!BitConverter.IsLittleEndian){
bodyLength = (short)(((bodyLength & 0xff)<< 8)|((bodyLength>>8) & 0xff));
Console.WriteLine("[Recv] Reverse lenBytes bodyLength=" + bodyLength);
}
if(readBuff.length < 2+bodyLength)
return;
Console.WriteLine("[Recv 1] length =" + readBuff.length);
Console.WriteLine("[Recv 2] readbuff=" + readBuff.ToString());
Console.WriteLine("[Recv 3] bodyLength=" + bodyLength);
readBuff.ReadInt16();
byte[] stringByte = new byte[bodyLength];
readBuff.Read(stringByte, 0, bodyLength);
string s = System.Text.Encoding.Default.GetString(stringByte);
Console.WriteLine("[Recv 4] s=" +s);
Console.WriteLine("[Recv 5] readbuff=" + readBuff.ToString());
if(readBuff.length > 2){
OnReceiveData();
}
}
public static void Send(string sendStr) {
byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);
Int16 len = (Int16)bodyBytes.Length;
byte[] lenBytes = BitConverter.GetBytes(len);
if(!BitConverter.IsLittleEndian){
Console.WriteLine("[Send] Reverse lenBytes");
lenBytes.Reverse();
}
byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
ByteArray ba = new ByteArray(sendBytes);
lock(writeQueue) {
writeQueue.Enqueue(ba);
if(writeQueue.Count==1)
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
}
Console.WriteLine("[Send] " + BitConverter.ToString(sendBytes));
}
public static void SendCallback(IAsyncResult ar){
try {
Socket socket = (Socket) ar.AsyncState;
int count = socket.EndSend(ar);
ByteArray ba;
lock(writeQueue) {
ba = writeQueue.First();
}
ba.readIdx += count;
if(ba.length == 0){
lock(writeQueue){
writeQueue.Dequeue();
if(writeQueue.Count>0) {
ba = writeQueue.First();
socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0, SendCallback, socket);
}
}
} else {
socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0, SendCallback, socket);
}
}
catch (SocketException ex){
Console.WriteLine("Socket Send fail" + ex.ToString());
}
}
public static void Close() {
socket.Close();
}
public static void Main(string[] args) {
Connect();
while(true) {
string sendStr = Console.ReadLine();
if(sendStr.Length==0)
continue;
else if(sendStr=="quit")
break;
else
Send(sendStr);
}
Close();
}
}
csc Echo.cs -reference:ByteArray.dll
Echo
Program.cs
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Linq;
class ClientState {
public Socket socket;
public ByteArray readBuff = new ByteArray();
}
class Program {
static Socket listenfd;
static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
public static void Main(string[] args) {
listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
listenfd.Bind(ipEp);
listenfd.Listen(0);
Console.WriteLine("[服务器]启动成功");
while(true){
if(listenfd.Poll(0, SelectMode.SelectRead)){
ReadListenfd(listenfd);
}
foreach(ClientState s in clients.Values){
Socket clientfd = s.socket;
if(clientfd.Poll(0, SelectMode.SelectRead)){
if(!ReadClientfd(clientfd)){
break;
}
}
}
System.Threading.Thread.Sleep(1);
}
}
public static void ReadListenfd(Socket listenfd) {
Console.WriteLine("[Accept]");
Socket clientfd = listenfd.Accept();
ClientState state = new ClientState();
state.socket = clientfd;
clients.Add(clientfd, state);
}
public static bool ReadClientfd(Socket clientfd) {
ClientState state = clients[clientfd];
ByteArray ba = state.readBuff;
if(ba.remain<8){
ba.ReSize(ba.length*2);
}
int count = clientfd.Receive(ba.bytes, ba.writeIdx, ba.remain, 0);
if(count == 0){
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Socket Close");
return false;
}
ba.writeIdx += count;
if (ba.length <= 2){
return true;
}
Int16 bodyLength = BitConverter.ToInt16(ba.bytes, 0);
if(ba.length < 2 + bodyLength)
return true;
ba.ReadInt16();
byte[] stringByte = new byte[bodyLength];
ba.Read(stringByte, 0, bodyLength);
string recvStr = System.Text.Encoding.Default.GetString(stringByte);
Console.WriteLine("Receive " + recvStr);
ba.CheckAndMoveBytes();
byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(recvStr);
Int16 len = (Int16)bodyBytes.Length;
byte[] lenBytes = BitConverter.GetBytes(len);
byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
foreach(ClientState cs in clients.Values) {
cs.socket.Send(sendBytes);
}
return true;
}
}
csc Program.cs -reference:ByteArray.dll
Program
|