我们要想实现两个程序在不同主机上进行相互通讯,我们就必须准确得标识这两个程序。我们知道对于一个程序来说其都有一个 PID(即进程控制符),虽然对于同一台主机上来说 PID 是唯一的,但是在不同主机之间,两个程序的 PID 那就不一定是唯一的了,其极有可能会发生重复,因此我们无法使用 PID 来标识不同主机上的程序。
于是Socket变应运而生,其使用 IP 地址标识了主机后,再使用端口标识了程序,也许你会问:既然 IP 标识了主机,那为什么不继续使用 PID 标识程序,那是因为当我们开启一个程序,但系统分配给这个程序的 PID 是一个随机的值,这样就导致了我们本地的主机程序 无法得知 我们所需连接的 远程主机程序 的 PID,从而无法完成连接。而端口却是固定的且是唯一的,一个端口只能被一个进程所占用,因此在网络通信中我们使用的端口标识程序。
综上所述,Socket是指 本地 IP 地址 和 远程 IP 地址 以及 本地端口号 和 远程端口号 的组合,其作用是标识不同主机间的程序,在计算机专业术语中它的意思是套接字,但我们光靠一个套接字显然不可能实现网络通讯,我们需要一些方法,这些方法最初起源于 1983 年加利福尼亚大学伯克利分校发布的 Berkeley Sockets API,后经微软、英特尔等大型公司的完善及规范,形成了一套标准,并在 Windows 上推出了 WinSock API,因此在网络编程中,其也被常常称为套接字接口,简称套接口。在.Net Framework的基础类库中,微软对 WinSock API 进行了进一步的包装,并为我们提供了强大的System.Net.Sockets命名空间,我们可以利用该命名空间轻松地完成网络编程。
备注:在早期,一个端口确实只能被一个进程占用,但是在后续的发展中进行了拓展,可以多进程同时占用一个端口,当然如果这样做了的话,那网络通信可能会出问题,这就需要我们自己进行处理。
最近发现有很多人认为 Socket 是指协议,在这儿我想很明确地告诉你们,那是错误的,记住Socket 不是协议。
协议为计算机网络中进行数据交换而建立的规则、标准或约定的集合(百度百科上是这么写的)。其中它有三个要素:语义、语法、时序,以下是我个人对这三个要素的理解:
语义:是指发送的每个数据包中的数据所代表的含义,例如第一个数据包发送的数据表示的是协议编号,第二个数据包发送的数据表示的是真正所需传递的数据 1,第三个数据包发送的数据表示的是真正所需传递的数据 2,这就是语义。其也可以看做是发送数据包的顺序。
语法:是指单个数据包的格式,例如每个数据包的前 8 个字节表示的是数据包的大小,而后面的 N 个字节表示的是该数据包所传递的数据,这就是语法,当然这语法也包括数据在转换成字节时所用到的编码类型等。
时序:是指整个服务器和客户端交互的流程,其中包括长连接与短连接、同步与异步等。
协议指的是通过语义 + 语法 + 时序所建立起来的规则、标准或约定的集合,而 Socket 仅仅是实现网络通讯的工具而已。
重要的事说三遍
套接字的分类:在System.Net.Sockets命名空间中,微软为我们提供了一个名为SocketType的枚举,其表示的是套接字的类型。SocketType枚举中共有 6 个成员,其中常用的有流式套接字(Stream)、数据报套接字(Dgram)、原始套接字(Raw)。流式套接字(Stream)必须被用于 TCP 协议中,这已经被微软写死,如果该套接字类型与协议类型不对应会报错;数据报套接字(Dgram)也已经被微软写死,只能用于 UDP 协议中;原始套接字(Raw)可以操纵网络层和传输层,一般被用于自定义协议中。
TCP 是一种面向连接的协议:由于 TCP 是一种面向连接的协议,因此对于 TCP 服务端来说,其必须创建用于监听的 Socket和用于连接的 Socket才能完成通讯,而由于 UDP 是一种面向无连接的协议,因此对于 UDP 服务端来说,其不需要监听连接。
网络通讯中所有数据都是以字节的形式进行传输:由于网络通讯中所有数据都是以字节的形式进行传输的,因此我们必须把相关数据都转化为 byte[] 的类型。其中 System 命名空间中的BitConverter类,为我们提供了一系列基础数据类型与字节数组相互转换的方法;另外对于字符串与字节数组的相互转换,我们可以使用 System.Text 命名空间中的Encoding类,并根据相应的编码方式来实现。
方法一:
Socket ListenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
方法二:
Socket ListenSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
AddressFamily:是一个枚举,指的是服务端地址的类型,对于 TCP 来说,我们提供的服务端地址一般是 IPV4 地址或 IPV6 地址,他们分别对应的是 InterNetwork 和 InterNetworkV6。
SocketType:是一个枚举,指的是套接字的类型,一般常用的有 Stream、Dgram 和 Raw,TCP 是使用流式套接字来实现传输的,因此我们在这里使用的是 Stream,如果我们使用了SocketType.Stream那 ProtocolType 必须使用ProtocolType.Tcp,另外 Dgram 是指数据报套接字,其被用于 UDP,如果我们使用了SocketType.Dgram那 ProtocolType 必须使用ProtocolType.Udp。
ProtocolType:是一个枚举,指的是协议类型,这里我们要用的是 Tcp,当然这个枚举里是没有 HTTP 的,由于 HTTP 是基于 TCP 的,因此如果要发 HTTP,我们需要使用的还是 Tcp。
IPAddress ServerIPAddress = IPAddress.Parse(ServerIP);
IPEndPoint ServerIPEndPoint = new IPEndPoint(ServerIPAddress, ServerPort);
ListenSocket.Bind(ServerIPEndPoint);
IPAddress:表示的是一个 IP 地址,我们可以使用IPAddress.Parse(String)的方法,将一个字符串形式的 IP 地址转换为一个 IPAddress 类。
IPEndPoint:表示的是一个由 IP 及端口组成的一个网络终结点,我们可以使用构造函数IPEndPoint(IPAddress, Int32)来进行创建,其所需的第二个参数指的是端口号。
Socket.Bind(EndPoint):该方法可以将 Socket ListenSocket 与 服务端的 IP 及端口 进行绑定,其中 EndPoint 表示的是一个网络地址,由于 IPEndPoint 继承自 EndPoint,所以我们我们可以直接将 IPEndPoint 带入方法中来完成该步骤。
ListenSocket.Listen(MaxListenNumber);
Socket.Listen(Int32):该方法可以将服务端的 Socket ListenSocket 设为监听状态,其所需的参数 Int32 表示的是连接队列的最大连接数。
方法一:
Socket ConnectSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
方法二:
Socket ConnectSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
该方法与之前说的创建服务端的 Socket ListenSocket 相同因此不再叙述。
设定服务端的 IP 及端口号
IPAddress ServerIPAddress = IPAddress.Parse(ServerIP);
IPEndPoint ServerIPEndPoint = new IPEndPoint(ServerIPAddress, ServerPort);
IPAddress 和 IPEndPoint:在发起连接前,我们需要设定服务端的地址,而设定服务端地址的方法也是利用 IPAddress 和 IPEndPoint 这两个类,由于该方法与之前讲的相同,我便不再叙说。
同步:
ConnectSocket.Connect(ServerIPEndPoint);
Socket.Connect(EndPoint):该方法可以使客户端向服务端以同步的方式发起一个连接请求。其所需的参数 EndPoint 表示的是服务端的地址,我们可以将 IPEndPoint 来带入。
异步:
object[] ConnectState = { ConnectSocket };
ConnectSocket.BeginConnect(ServerIPEndPoint, ConnectCallBack, ConnectState);
private static void ConnectCallBack(IAsyncResult AsyncResult)
{
object[] ConnectState = (object[])AsyncResult.AsyncState;
Socket ConnectSocket = (Socket)ConnectState[0];
ConnectSocket.EndConnect(AsyncResult);
}//void ConnectCallBack(IAsyncResult AsyncResult)
Socket.BeginConnect(EndPoint, AsyncCallback, Object):该方法可以使客户端向服务器以异步的方式发起一个连接请求。该方法需要 3 个参数:第一个参数 EndPoint,表示的是服务端的地址,我们可以将IPEndPoint来带入;第二个参数 AsyncCallback表示的是一个异步委托,其原型是 delegate void AsyncCallback(IAsyncResult ar),因此回调函数应是 一个无返回值 void 类型 且 所需参数是一个 IAsyncResult 的函数,由于该参数是一个委托,因此我们可以直接将回调函数名直接带入即可;第三个参数 Object表示所需传递至回调函数的参数,由于这里只能带入一个参数,我们需要将多个参数打包成一个 object[],然后带入。
IAsyncResult 和 IAsyncResult.AsyncState:IAsyncResult 是一个接口,表示的是一个异步操作的状态,我们可以使用 IAsyncResult 的 AsyncState 属性以及强制转换类型的方法来获取 在 BeginConnect(EndPoint, AsyncCallback, Object) 函数中所带入的第三个参数 object 的 值。
Socket.EndConnect(IAsyncResult):该方法可以结束一个客户端向服务端发起的异步连接请求,其所需的参数是一个IAsyncResult。
同步:
Socket ConnectSocket = ListenSocket.Accept();
Socket.Accept():该方法可以使服务端接受客户端的连接请求,并生成一个服务端用于连接客户端的 Socket ConnectSocket。
异步:
object[] AcceptState = { ListenSocket };
ListenSocket.BeginAccept(AcceptCallBack, AcceptState);
private static void AcceptCallBack(IAsyncResult AsyncResult)
{
object[] AcceptState = (object[])AsyncResult.AsyncState;
Socket ListenSocket = (Socket)AcceptState[0];
Socket ConnectSocket = ListenSocket.EndAccept(AsyncResult);
}//void AcceptCallBack(IAsyncResult AsyncResult)
Socket.BeginAccept(AsyncCallback, Object):该方法可以 以异步的方式 使服务端接收来自客户端发来的连接请求,并生成一个服务端用于连接客户端的 Socket ConnectSocket。该方法需要 2 个参数:第一个参数 AsyncCallback和之前说的一样,表示是的一个异步委托,我们只需带入回调函数名即可;第二个参数 Object表示所需传递至回调函数的参数。
Socket.EndAccept(IAsyncResult):该方法可以终止 使服务端接受客户端的连接请求 的异步操作,其所需的参数是一个IAsyncResult,该方法具有一个返回值,返回值即为 Socket ConnectSocket。
同步:
ConnectSocket.Send(ClientData, 0, ClientData.Length, SocketFlags.None);
Socket.Send(Byte[], Int32, Int32, SocketFlags):该方法可以将要发送的 byte[] 形式的数据写入连接套接字中,从而实现数据的发送。该方法需要 4 个参数:第一个参数 Byte[]表示的是所需发送的数据对象;第二个参数 Int32表示的是发送数据的起始位置;第三个参数 Int32表示的是发送数据的大小;第四个参数 SocketFlags是一个枚举,其指的是套接字的收发行为,一般我们只需使用 None 即可。
异步:
object[] SendState = { ConnectSocket, ClientData };
ConnectSocket.BeginSend(ClientData, 0, ClientData.Length, SocketFlags.None, SendCallBack, SendState);
private static void SendCallBack(IAsyncResult AsyncResult)
{
object[] SendState = (object[])AsyncResult.AsyncState;
Socket ConnectSocket = (Socket)SendState[0];
byte[] ClientData = (byte[])SendState[1];
ConnectSocket.EndSend(AsyncResult);
}//void SendCallBack(IAsyncResult AsyncResult)
Socket.BeginSend(Byte[], Int32, Int32, SocketFlags, AsyncCallback, Object):该方法可以 以异步的方式 将要发送的数据写入连接套接字中。该方法需要 6 个参数::第一个参数 Byte[]表示的是所需发送的数据对象;第二个参数 Int32表示的是发送数据的起始位置;第三个参数 Int32表示的是发送数据的大小;第四个参数 SocketFlags是一个枚举,其指的是套接字的收发行为,一般我们只需使用 None 即可;第五个参数 AsyncCallback表示的是一个异步委托,我们只需带入回调函数名即可;第六个参数 Object表示的是所需传递给回调函数的数据。
Socket.EndSend(IAsyncResult):该方法可以终止 将数据 byte[] 写入套接字 的异步操作,其所需的参数是一个IAsyncResult,该方法具有一个 int 类型的返回值,表示的是已经写入套接字的数据大小。
同步:
ConnectSocket.Receive(ClientData, 0, ClientData.Length, SocketFlags.None);
Socket.Receive(Byte[], Int32, Int32, SocketFlags):该方法可以从连接套接字中取出远程发来的数据 byte[]。该方法需要 4 个参数:第一个参数 Byte[]表示的是接收数据的数据对象;第二个参数 Int32表示的是接收数据的起始存放位置;第三个参数 Int32表示的是所需接收数据的大小;第四个参数 SocketFlags是一个枚举,其指的是套接字的收发行为,一般我们只需使用 None 即可。
异步:
object[] ReceiveState = { ConnectSocket, ClientData };
ConnectSocket.BeginReceive(ClientData, 0, ClientData.Length, SocketFlags.None, ReceiveCallBack, ReceiveState);
private static void ReceiveCallBack(IAsyncResult AsyncResult)
{
object[] ReceiveState = (object[])AsyncResult.AsyncState;
Socket ConnectSocket = (Socket)ReceiveState[0];
byte[] ClientData = (byte[])ReceiveState[1];
ConnectSocket.EndReceive(AsyncResult);
}//void ReceiveCallBack(IAsyncResult AsyncResult)
Socket.BeginReceive(Byte[], Int32, Int32, SocketFlags, AsyncCallback, Object):该方法可以 以异步的方式 从连接套接字中取出远程发来的数据 byte[]。该方法需要 6 个参数:第一个参数 Byte[]表示的是接收数据的数据对象;第二个参数 Int32表示的是接收数据的起始存放位置;第三个参数 Int32表示的是所需接收数据的大小、第四个参数 SocketFlags是一个枚举,其指的是套接字的收发行为,一般我们只需使用 None 即可;第五个参数 AsyncCallback表示的是一个异步委托,我们只需带入回调函数名即可;第六个参数 Object表示的是所需传递给回调函数的数据。
Socket.EndReceive(IAsyncResult):该方法可以终止 从连接套接字中取出远程发来的数据 的异步操作,其所需的参数是一个IAsyncResult,该方法具有一个 int 类型的返回值,表示的是从连接套接字中已经取出的数据大小。
同步:
ConnectSocket.Disconnect(true);
Socket.Disconnect(Boolean):该方法可以关闭套接字连接,并允许设定是否可以重用套接字,其所需的参数 Boolean 是一个布尔值,为 true 则表示可以重用套接字,为 false 则表示不可重用套接字。
异步:
object[] DisconnectState = { ConnectSocket };
ConnectSocket.BeginDisconnect(true, DisconnectCallBack, DisconnectState);
private static void DisconnectCallBack(IAsyncResult AsyncResult)
{
object[] DisconnectState = (object[])AsyncResult.AsyncState;
Socket ConnectSocket = (Socket)DisconnectState[0];
ConnectSocket.EndReceive(AsyncResult);
}//void DisconnectCallBack(IAsyncResult AsyncResult)
Socket.BeginDisconnect(Boolean, AsyncCallback, Object):该方法可以 以异步的方式 关闭套接字连接。该方法有 3 个参数:第一个参数 Boolean是一个布尔值,为 true 则表示可以重用套接字,为 false 则表示不可重用套接字;;第二个参数 AsyncCallback表示的是一个异步委托,我们只需带入回调函数名即可;第三个参数 Object表示的是所需传递给回调函数的数据。
Socket.EndDisconnect(IAsyncResult):该方法可以终止 关闭套接字连接 的异步操作,其所需的参数是一个IAsyncResult。
在 System.Net.Sockets 命名空间还有其他许多重要的类、方法等,例如能增强异步通讯的 SocketAsyncEventArgs 类等,其相关 API 文档都在 MSDN 里,网址是https://msdn.microsoft.com/zh-cn/library/system.net.sockets(v=vs.110).aspx
短连接:短连接是指服务端与客户端每次完成通讯后,就断开连接套接字 Socket ConnectSocket 的连接,同时每次需要服务端与客户端产生通讯的时候,都要重新创建连接套接字 Socket ConnectSocket,最典型的短连接应该就是 HTTP 了吧(当然从 HTTP/1.1 起,HTTP 默认使用长连接)
长连接:用于对于客户端来说,其只需要一个连接套接字 Socket ConnectSocket 就能完成与服务端所有的通讯,因此对于长连接来说,客户端只需在最开始创建一个连接套接字 Socket ConnectSocket,便可以和服务端反复通讯多次。而当一个连接套接字 Socket ConnectSocket 长时间存在,其便会出现两个问题:1、当 Socket ConnectSocket 的连接如果被中断后,我们再去使用这个 Socket ConnectSocket 进行通讯时,其便会出错;2、如果服务端不断地接收客户端的连接并创建相应的连接套接字 Socket ConnectSocket,却又不去关闭已经失效的 Socket ConnectSocket,那么服务端迟早将会挂掉。由于会出现以上这两个问题,为此便引申出了 KeepAlive 机制。
KeepAlive 机制:简单地来讲就是让一台主机每隔一段时间不停地向另一台远程主机发送连接请求(心跳包),以确认对方是否仍处于连接状态,如果发现对方长时间不应答,便关闭与对方连接。理论上来说服务端和客户端都可以向对方发送心跳包,但一般来说都是由客户端向服务端发送心跳包。在 TCP 中,KeepAlive 机制默认是如果对方 2 小时不应答,则会断开连接,但是由于 2 小时时间过长,因此一般我们都要重写该机制。