1.知识点
- socket类型:
Internet 套接字(最经典常用):
①流格式套接字SOCK_STREAM:向连接的套接字,TCP( HTTP、FTP);
是可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。
传输过程:提前规划好路径(虚电路);数据报同一路径
②数据报格式套接字SOCK_DGRAM:无连接的套接字,UDP(DNS、即时聊天软件)
是不可靠的、不按顺序传递的、以追求速度为目的的套接字。
传输过程:不规划路径,每个数据包可能走不同的路径 - IP(局域网)、MAC(主机)、端口号(程序)
- socket缓冲区:
①每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
②write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。
③TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
④read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
⑤这些I/O缓冲区特性可整理如下:
I/O缓冲区在每个TCP套接字中单独存在;
I/O缓冲区在创建套接字时自动生成;
即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
关闭套接字将丢失输入缓冲区中的数据。 - 阻塞模式:
所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。TCP套接字默认情况下是阻塞模式,当然你也可以更改为非阻塞模 - TCP粘包问题:
客户端发送的多个数据包被当做一个数据包接收,也称数据的无边界性。 - TCP三次握手建立连接,四次握手断开连接。
- TCP与UDP:
①TCP点对点需要连接;UDP像邮寄包裹,只要地址不需要连接
②TCP 的速度无法超越 UDP,但在收发某些类型的数据时有可能接近 UDP。例如,每次交换的数据量越大,TCP 的传输速率就越接近于 UDP。 - TCP的状态 (SYN, FIN, ACK, PSH, RST, URG):
在TCP层,有个FLAGS字段,这个字段有以下几个标识:SYN, FIN, ACK, PSH, RST, URG.
其中,对于我们日常的分析有用的就是前面的五个字段。
SYN表示建立连接,
FIN表示关闭连接,
ACK表示响应,
PSH表示有 DATA数据传输,
RST表示连接重置,
URG表示紧急指针是否有效(紧急处理的数据)。
2.名词
- 网络编程:就是编写程序使两台联网的计算机相互交换数据。
- socket :原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,socket 就是用来连接到因特网的工具。
- UNIX/Linux 中的 socket : UNIX/Linux 中的一切都是文件!为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。
- Window 系统中的 socket : Windows 也有类似“文件描述符”的概念,但通常被称为“文件句柄”。 与 UNIX/Linux 不同的是,Windows 会区分 socket 和文件,Windows 就把 socket 当做一个网络连接来对待,因此需要调用专门针对 socket 而设计的数据传输函数,针对普通文件的输入输出函数就无效了。
- 吞吐量:是指当前流量值,当前流量值由当前网络数据包大小和当前数据包个数决定。
- 网络带宽:由网卡速率,全双工半双工决定,也就是人们熟知的百兆带宽,千兆带宽。
- 网络负载:以百分比计算,实际上也就是当前吞吐值/网络带宽,反映了当前负载大小,百分比越高越接近于网络饱和。
- 丢包率:由源发送报文个数和目的实际接收报文个数决定,一般在网络设备接近于饱和后由于性能不足导致丢包,如果丢包率过高,说明网络内部性能不足,出现瓶颈。
- 数据速率大小:数据速率大小指的是网络实际业务数据传输速率,而这个数据速率其实往往与数据本身有关,比如数据压缩比率,数据编码格式,数据传输方式有关。
- 网络边界:ip地址,由此入口进入电脑。
- 数据边界:数据长度。
- 开放式系统:以多个标准为依据设计的系统,既标准化
- socket编程实现回声客户端:
所谓“回声”,是指客户端向服务器发送一条数据,服务器再将数据原样返回给客户端,就像声音一样,遇到障碍物会被“反弹回来”。 - 如何让服务器端持续监听客户端的请求?使用 while 循环。
- 优雅地断开TCP连接:
默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。 - socket编程实现文件传输功能:
文件大小不确定,有可能比缓冲区大很多,调用一次receive()/send() 函数不能完成文件内容的发送。使用while循环,注意循环结束标志。 - 网络数据的大小端问题:
不同 CPU 保存和解析数据的方式不同(主流的 Intel 系列 CPU 为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。网络字节序统一为大端序。
图1:整数 0x12345678 的大端序字节表示
图2:整数 0x12345678 的小端序字节表示 - 在socket编程中使用域名:
客户端中直接使用 IP 地址会有很大的弊端,一旦 IP 地址变化(IP 地址会经常变动),客户端软件就会出现错误。而使用域名会方便很多,注册后的域名只要每年续费就永远属于自己的,更换 IP 地址时修改域名解析即可,不会影响软件的正常使用。
有关知识:域名注册、域名解析、host 文件、DNS服务器等
域名仅仅是 IP 地址的一个助记符,目的是方便记忆,通过域名并不能找到目标计算机,通信之前必须要将域名转换成 IP 地址。
string web = "www.baidu.com";
IPHostEntry host = Dns.GetHostEntry(web);
IPAddress ip = host.AddressList[0];
Console.WriteLine(ip.ToString());
Console.ReadKey();
3.socket实现过程
- 无论是服务器还是客户端,创建一个 SOCKET 对象,创建方法一致。
Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//监控 ip4 地址,套接字类型为 TCP ,协议类型为 TCP
其有三个构造函数
public Socket(SocketInformation socketInformation);
public Socket(SocketType socketType, ProtocolType protocolType);
public Socket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType);
第一个构造函数SocketInformation对象保存的是Socket(SocketType, ProtocolType),与第二个一样
- Bind() 绑定与 Connect() 连接
Bind() 用于绑定 IPEndPoint 对象,在服务端使用
public void Bind (System.Net.EndPoint localEP);
IPEndPoint目标终点,IP地址和端口号的组合。
Connect() 在客户端使用,用于连接服务端。
创建 Socket 对象后,接着 绑定本地Socket/连接服务端。
IPAddress ip = IPAddress.Any;
IPAddress提供4个只读字段:
Any 用于代表本地系统可用的任何IP地址
Broadcase用于代表本地网络的IP广播地址
Loopback用于代表系统的回送地址
None用于代表系统上没有网络接口
> IPAddress.Any表示本机ip,换言之,如果服务器绑定此地址,则表示侦听本机所有ip对应的那个端口(本机可能有多个ip或只有一个ip)
> 比如双网卡机器,内网ip为192.168.0.1,外网ip为120.210.1.1,服务器可以同时监听192.168.0.1:80和120.210.1.1:80。
------------------------------------------------------------------------------------------------------------
IPAddress iP = IPAddress.Parse("127.0.0.1");//把ip地址字符串转换为IPAddress类型的实例
IPEndPoint iPEndPoint = new IPEndPoint(iP, 2300);//用指定的端口和ip初始化IPEndPoint类的新实例
serverSocket.Bind(iPEndPoint);//服务端绑定ip和端口
//serverSocket.Bind(new IPEndPoint(iP, 2300))
serverSocket.Connect(iPEndPoint);//客户端创建与远程主机的连接
- Listen() 监听请求连接 和 Accept() 接收连接请求
Listen():监控所有发送到此主机的、特定端口的连接请求。服务端使用,客户端不需要
public void Listen (int backlog);//backlog 参数指定可排队等待接受的传入连接的数量,即挂起的连接队列的最大长度。
serverSocket.Listen(10); //开始监听
Accept()
Accept() 以同步方式监听套接字,在连接请求队列中提取第一个挂起的连接请求,然后创建并返回一个新的 Socket 对象。
serverSocket.Listen(10);//开始监听
//接受到client连接,为此连接建立新的socket,并接受信息
Socket temp = serverSocket.Accept();//为新建连接创建新的socket复制代码
........
temp.Close(); //关闭连接
- Receive() 与 Send()
Receive()
使用示例
string recvStr = "";
byte[] recvBytes = new byte[1024];
int bytes;
//Receive(Byte[], Int32, SocketFlags) Byte 类型的数组,它是存储接收到的数据的位置、要接收的字节数、socketFlags 默认值为 0 或 None
bytes = temp.Receive(recvBytes, recvBytes.Length, 0);//从客户端接受信息,返回已成功读取的字节数
recvStr += Encoding.ASCII.GetString(recvBytes, 0, bytes);
Send()
使用示例
string str = "hello";
byte[] a = Encoding.UTF8.GetBytes(str);
send = socket.Send(a, 0);
发送/接收 都是使用 byte[] 字节流,所以接收时要进行转换。
- 释放资源
有 Accept 释放和 Socket 的释放。
Accept 是连接对象,一个 Socket 可能有数十个 Accept 连接。
使用 Shutdown( ) 方法可以 禁止 Asscpt 对象的操作(禁用某个 Socket 对象 的发送和接收)。
public void Shutdown (System.Net.Sockets.SocketShutdown how);
SocketShutdown 是一个 enum 类型。
temp.Shutdown(SocketShutdown.Receive); //禁止接收
Send 禁止对此发送Socket。
Receive 禁用对此接收Socket。
Both 禁用发送和接收对此Socket。
close();//会直接释放资源,Accept 和 Socket 对象都可以使用。使用后对象将彻底释放。
- 线程
服务端
socketWatch.Listen(10);//设置监听
//创建监听线程
Thread thread = new Thread(Listen);
thread.IsBackground = true;
thread.Start(socketWatch);
客户端
socketSend.Connect(IPEndPoint);
//开启新的线程,不停的接收服务器发来的消息
Thread c_thread = new Thread(Received);
c_thread.IsBackground = true;
c_thread.Start();
7.UDP与TCP
①UDP中的服务器端和客户端没有连接
UDP 不像 TCP,无需在连接状态下交换数据,因此基于 UDP 的服务器端和客户端也无需经过连接过程。不必调用connect() 、 listen() 和 accept() 函数。UDP 中只有创建套接字的过程和数据交换的过程。
②UDP服务器端和客户端均只需1个套接字
TCP 中,套接字是一对一的关系。如要向 10 个客户端提供服务,那么除了负责监听的套接字外,还需要创建 10 套接字。但在 UDP 中,不管是服务器端还是客户端都只需要 1 个套接字。之前解释 UDP 原理的时候举了邮寄包裹的例子,负责邮寄包裹的快递公司可以比喻为 UDP 套接字,只要有 1 个快递公司,就可以通过它向任意地址邮寄包裹。同样,只需 1 个 UDP 套接字就可以向任意主机传送数据。
③基于UDP的接收和发送函数
创建好 TCP 套接字后,传输数据时无需再添加地址信息,因为 TCP 套接字将保持与对方套接字的连接。换言之,TCP 套接字知道目标地址信息。但 UDP 套接字不会保持连接状态,每次传输数据(接收发送)都要添加目标地址信息,这相当于在邮寄包裹前填写收件人地址。
//发送
public int SendTo(byte[] buffer, int size, SocketFlags socketFlags, EndPoint remoteEP);
//buffer:System.Byte 类型的数组,它包含要发送的数据。
//size:要发送的字节数。
//socketFlags:System.Net.Sockets.SocketFlags 值的按位组合。
//remoteEP:System.Net.EndPoint,它表示数据的目标位置。
client.SendTo(data, data.Length, SocketFlags.None, ip);
//接受
public int ReceiveFrom(byte[] buffer, ref EndPoint remoteEP);
//buffer: System.Byte 类型的数组,它是存储接收到的数据的位置。
//remoteEP:按引用传递的 System.Net.EndPoint,表示远程服务器。
recv = serve.ReceiveFrom(revData, ref Remote);
补充:
IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
EndPoint Remote = (EndPoint)sender;
string recvDateSucceed = string.Format("服务器已收到.");
sendData = Encoding.Unicode.GetBytes(recvDateSucceed);//将string以unicode编码为一个字节序列