个人对 Socket 和协议的理解 以及 使用 System.Net.Sockets 命名空间编写简单的 TCP 通讯程序

Socket

我们要想实现两个程序在不同主机上进行相互通讯,我们就必须准确得标识这两个程序。我们知道对于一个程序来说其都有一个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仅仅是实现网络通讯的工具而已。

Socket不是协议!Socket不是协议!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类,并根据相应的编码方式来实现。

服务端和客户端通讯的基本流程(TCP)

服务端:创建用于监听的Socket ListenSocket

方法一:

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。

服务端:将Socket ListenSocket与 服务端的IP及端口 进行绑定
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带入方法中来完成该步骤。

服务端:将Socket ListenSocket设为监听状态,开始监听
ListenSocket.Listen(MaxListenNumber);

Socket.Listen(Int32):该方法可以将服务端的Socket ListenSocket设为监听状态,其所需的参数Int32表示的是连接队列的最大连接数。

客户端:创建连接Socket ConnectSocket

方法一:

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

同步:

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];
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值