网络编程

地址信息设置

struct sockaddr 和 struct sockaddr_in

以IPv4为例介绍网络的地址结构。主要涉及的结构体有struct in_addrstruct sockaddrstruct sockaddr_in。其中struct sockaddr是一种通用的地址结构,它可以描述一个IPv4或者IPv6的结构,所有涉及到地址的接口都使用了该类型的参数,但是过于通用的结果是直接用它来描述一个具体的IP地址和端口号十分困难。所以用户一般先使用struct sockaddr_in来构造地址,再将其进行强制类型转换成struct sockaddr以作为网络接口的参数。

大小端转换


域名和IP地址的对应关系

IP层通过IP地址的结构进行路由选择最终找到一条通往目的地的路由,但是一些著名的网站如果采用IP地址的方式提供地址,用户将无法记忆,所以更多的时候需要一个方便人类记忆的域名(比如www.kernel.org)作为其实际IP地址(145.40.73.55)的别名,显然我们需要一种机制去建立域名和IP地址的映射关系,一种方法是修改本机的hosts文件/etc/hosts,但是更加通用的方案是利用DNS协议,去访问一个DNS服务器,服务器当中存储了域名和IP地址的映射关系。与这个操作相关的函数是gethostbyname,下面是其用法:

TCP通信

TCP网络编程

socket

socket函数用于创建一个socket设备。调用该函数时需要指定通信的协议域、套接字类型和协议类型。一般根据选择TCP或者UDP有着固定的写法。socket函数的返回值是一个非负整数,就是指向内核socket设备的文件描述符。

connect

客户端使用connect来建立和TCP服务端的连接。

bind

listen

一旦启用了listen之后,操作系统就知道该套接字是服务端的套接字,操作系统内核就不再启用其发送和接收缓冲区,转而在内核区维护两个队列结构:半连接队列全连接队列。半连接队列用于管理成功第一次握手的连接,全连接队列用于管理已经完成三次握手的队列。backlog在有些操作系统用来指明半连接队列和全连接队列的长度之和,一般填一个正数即可。如果队列已经满了,那么服务端受到任何再发起的连接都会直接丢弃(大部分操作系统中服务端不会回复RST,以方便客户端自动重传)

使用netstat -an命令可以查看主机上某个端口的监听情况。

accept

accept函数由服务端调用,用于从全连接队列中取出下一个已经完成的TCP连接。如果全连接队列为空,那么accept会陷入阻塞。一旦全连接队列中到来新的连接,此时accept操作就会就绪,这种就绪是读操作就绪,所以可以使用select函数的读集合进行监听。当accept执行完了之后,内核会创建一个新的套接字文件对象,该文件对象关联的文件描述符是accept的返回值,文件对象当中最重要的结构是一个发送缓冲区和接收缓冲区,可以用于服务端通过TCP连接发送和接收TCP段。

区分两个套接字是非常重要的。通过把旧的管理连接队列的套接字称作监听套接字,而新的用于发送和接收TCP段的套接字称作已连接套接字。通常来说,监听套接字会一直存在,负责建立各个不同的TCP连接(只要源IP、源端口、目的IP、目的端口四元组任意一个字段有区别,就是一个新的TCP连接),而某一条单独的TCP连接则是由其对应的已连接套接字进行数据通信的。

客户端使用close关闭套接字或者服务端使用close关闭已连接套接字的时候就是主动发起断开连接四次挥手的过程。

需要特别注意的是,addrlen参数是一个传入传出参数,所以使用的时候需要主调函数提前分配好内存空间。

send和recv

sendrecv用于将数据在用户态空间和内核态的缓冲区之间进行传输,无论是客户端还是服务端均可使用,但是只能用于TCP连接。将数据拷贝到内核态并不意味着会马上传输,而是会根据时机再由内核协议栈按照协议的规范进行分节,通常缓冲区如果数据过多会分节成MSS的大小,然后根据窗口条件传输到网络层之中。

下面是一个完整的客户端和服务端通信的例子:

需要特别注意的是,sendrecv的次数和网络上传输的TCP段的数量没有关系,多次的sendrecv可能只需要一次TCP段的传输。另外一方面,TCP是一种流式的通信协议,消息是以字节流的方式在信道中传输,这就意味着一个重要的事情,消息和消息之间是没有边界的。在不加额外约定的情况下,通信双方并不知道发送和接收到底有没有接收完一个消息,有可能多个消息会在一次传输中被发送和接收("粘包"),也有有可能一个消息需要多个传输才能被完整的发送和接收("半包")。

TIME_WAIT和setsockopt

recv和send的标志

  1. MSG_DONTWAIT

  2. MSG_PEEK

UDP通信

UDP网络编程

sendto和recvfrom

epoll

epoll的基本函数

select一样,epoll也是一种IO多路复用机制,它可以监听多个设备的就绪状态,让进程或者线程只在有事件发生之后再执行真正的读写操作。epoll可以在内核态空间当中维持两个数据结构:监听事件集合就绪事件队列。监听事件集合用来存储所有需要关注的设备(即文件描述符)和对应操作(比如读、写、挂起和异常等等),当监听的设备有事件产生时,比如网卡上接收到了数据并传输到了缓冲区当中时,硬件会采用中断等方式通知操作系统,操作系统会将就绪事件拷贝到就绪事件队列中,并且找到阻塞在epoll_wait的线程,让其就绪。监听事件集合通常是一个红黑树,就绪事件队列是一个线性表。

select相比,epoll的优势如下:

有了这些优势之后,epoll逐渐取代了select的市场地位,尤其是在管理巨大量连接的高并发场景中,epoll的性能要远超select

epoll的触发方式

epoll 支持两种主要的事件触发方式:水平触发(Level Triggered, LT)和边缘触发(Edge Triggered, ET)。这两种方式在如何通知应用程序文件描述符就绪方面有显著不同。

水平触发

水平触发(Level Triggered, LT)是 epoll 的默认工作模式,其行为模式与传统的 selectpoll 类似。

工作原理与特性:

优点:

缺点:

使用场景:

边缘触发

边缘触发 (Edge Triggered, ET)是一种更高效但也更复杂的模式。

工作原理与特性:

优点:

缺点:

使用场景:

LT 与 ET 的区别

特性水平触发 (LT)边缘触发 (ET)
通知时机只要条件满足就通知仅在状态发生变化时通知一次
持续性若未完全处理,下次 epoll_wait 仍会通知若未完全处理,通常不会再次通知,除非有新事件发生
处理要求可以不一次性处理完所有数据必须一次性处理完所有数据(读/写直到 EAGAIN
编程复杂度相对简单较高,容易出错
效率相对较低,可能有冗余通知相对较高,通知次数少
FD模式可用于阻塞或非阻塞 FD强烈建议(通常是必须)与非阻塞 FD 一起使用
健壮性对事件处理遗漏的容忍度较高对事件处理遗漏非常敏感,可能导致数据丢失或程序挂起