Linux基础(四)——网络编程
一、网络结构模式

在C/S结构中,客户端和服务器之间通过网络进行通信。客户端通常是一个运行在用户计算机上的应用程序或浏览器,它向服务器发送请求并等待响应。服务器则是一个运行在服务器计算机上的应用程序,它接收客户端请求并提供服务。
C/S结构的优点包括:
可以将应用程序的处理任务分配给不同的计算机,使得处理负载更加均衡,提高系统的可伸缩性和性能;
可以通过服务器控制和管理应用程序,提高系统的安全性和稳定性;
可以实现更高级别的应用程序功能和复杂度,例如数据库管理系统。
C/S结构的缺点包括:
客户端和服务器之间需要网络连接,因此需要考虑网络带宽和延迟等因素;
应用程序的部署和维护需要更多的工作,例如安装和配置客户端软件和服务器软件;
由于服务器需要处理来自多个客户端的请求,因此服务器的设计和实现需要更高的技术水平和经验。
总之,C/S结构是一种常见的计算机系统架构模式,具有一些优点和缺点,适用于需要高级别应用程序功能和复杂度的网络应用程序。

在B/S结构中,浏览器作为客户端向服务器发送请求并接收响应。浏览器通常是一个Web浏览器,例如Chrome或Firefox。服务器是一个Web服务器,它接收浏览器请求并提供服务。
B/S结构的优点包括:
可以跨平台和跨浏览器运行,因为Web浏览器是几乎所有计算机和移动设备上的标准软件;
应用程序的部署和维护比C/S结构更方便,因为只需要在Web服务器上安装和配置应用程序;
可以轻松地更新应用程序和内容,因为应用程序和内容都存储在服务器上。
B/S结构的缺点包括:
应用程序的响应速度受到网络带宽和延迟等因素的影响;
应用程序的功能和复杂度受到Web浏览器的限制,因为Web浏览器只能执行JavaScript等有限的客户端脚本语言。
总之,B/S结构是一种常见的计算机系统架构模式,适用于基于Web的应用程序。它具有一些优点和缺点,可以根据具体应用场景和需求来选择使用。
二、MAC地址、IP地址、端口

MAC地址是由网络接口控制器(NIC)分配的,用于标识网络上的设备,如计算机、路由器、交换机等。每个网络接口控制器都有一个唯一的MAC地址,它在制造过程中被写入,无法更改。
MAC地址的前24位(前6个十六进制数)称为组织唯一标识符(OUI),用于标识厂商或组织。后24位为设备标识符,用于标识特定设备。
在网络通信中,MAC地址用于在局域网中识别和定位设备。当一个设备发送数据包时,它会在数据包中包含目标设备的MAC地址,以便路由器或交换机可以将数据包传递给正确的设备。因此,MAC地址在局域网中起着非常重要的作用。
在计算机上,可以使用命令行工具(如ipconfig/ifconfig)来查看MAC地址。在路由器或交换机上,可以使用命令行工具(如show mac-address-table)来查看MAC地址。


IP地址编址方式有两种:IPv4和IPv6。
IPv4:IPv4使用32位二进制数字编址,共有42亿个地址。IPv4地址通常使用点分十进制表示法表示。IPv4地址分为A、B、C、D和E五类地址,其中A、B、C三类地址用于互联网上的主机,D和E地址则是特殊用途的地址。IPv4地址还有私有地址和保留地址等特殊地址。
IPv6:IPv6使用128位二进制数字编址,共有340万亿亿亿亿个地址。IPv6地址通常使用冒号分隔的八组十六进制数表示。IPv6地址采用一个新的编址方式,将地址空间分为子网前缀、接口标识符和全球路由前缀三个部分。IPv6还支持任意长度的前缀子网掩码,可以更灵活地分配地址。
总之,IP地址是用于在互联网上标识设备的32位(IPv4)或128位(IPv6)二进制数字,可以使用点分十进制或冒号分隔的十六进制数表示。IPv4和IPv6有不同的编址方式,各有优缺点,可以根据具体需求选择使用。

A类地址:A类地址是以0开头的8位二进制数作为网络地址,其余24位二进制数作为主机地址,共有2^24-2个主机地址。A类地址范围是1.0.0.0~126.0.0.0,其中1.0.0.0是保留地址,用于识别本地地址。A类地址适用于大型网络,如全球互联网。
B类地址:B类地址是以10开头的16位二进制数作为网络地址,其余16位二进制数作为主机地址,共有2^16-2个主机地址。B类地址范围是128.0.0.0~191.255.0.0。B类地址适用于中等规模的网络。
C类地址:C类地址是以110开头的24位二进制数作为网络地址,其余8位二进制数作为主机地址,共有2^8-2个主机地址。C类地址范围是192.0.0.0~223.255.255.0。C类地址适用于小型网络,如局域网。
D类地址:D类地址用于多播(Multicast),即一次向多个设备发送数据。D类地址是以1110开头的32位二进制数,范围是224.0.0.0~239.255.255.255。
E类地址:E类地址是保留地址,用于实验和研究,范围是240.0.0.0~255.255.255.255。
特殊的网址包括:
0.0.0.0:表示本地主机,通常用于初始化网络接口。
127.0.0.1:表示本地主机环回地址,也称为localhost,用于测试网络接口是否正常。
255.255.255.255:表示广播地址,用于向同一子网内的所有主机广播消息。
169.254.x.x:表示自动配置地址,也称为APIPA(Automatic Private IP Addressing),用于在没有DHCP服务器的情况下自动分配IP地址。
总之,A、B、C、D、E五类IP地址用于不同规模和用途的网络,特殊的网址包括本地主机地址、环回地址、广播地址和自动配置地址等。

子网掩码的作用是将IP地址划分成网络地址和主机地址两部分,以便进行子网划分和路由选择。子网掩码中的连续的1表示网络部分,连续的0表示主机部分。例如,子网掩码为255.255.255.0的IP地址,前24位是连续的1,表示网络部分,后8位是0,表示主机部分,可以划分成256个子网,每个子网可以容纳256个主机。
子网掩码与IP地址结合使用,通过逻辑与运算来确定网络地址和主机地址。例如,如果IP地址为192.168.1.100,子网掩码为255.255.255.0,则通过逻辑与运算,可以得到网络地址为192.168.1.0,主机地址为0.0.0.100。
子网掩码的长度决定了网络的大小,长度越长,网络越小,可容纳的主机数量越少。在进行子网划分时,需要根据实际需求确定子网掩码的长度和子网的数量。
总之,子网掩码是用于划分IP地址的技术,用于将一个IP地址划分成网络地址和主机地址两个部分。它由32位二进制数组成,与IP地址结合使用,通过逻辑与运算来确定网络地址和主机地址。子网掩码的长度决定了网络的大小,需要根据实际需求进行设置。

端口类型是指不同用途的端口。根据端口的用途,端口可以分为以下三种类型:
众所周知端口(Well-known Port):指0~1023的端口,这些端口被指定为某些服务的标准端口,如HTTP服务的80端口、FTP服务的21端口、SSH服务的22端口等。这些端口是被广泛认可和使用的,应用程序可以直接使用这些端口来访问相应的服务。
注册端口(Registered Port):指1024~49151的端口,这些端口是被分配给某些服务的,但是并没有被正式指定为标准端口。这些端口通常被用于一些特定的应用程序或服务。
动态端口(Dynamic Port):指49152~65535的端口,这些端口是由操作系统动态分配的,并且通常只在一次会话中使用。当应用程序需要建立一个网络连接时,操作系统会自动分配一个可用的动态端口,用于该会话的数据传输。
总之,端口是计算机网络中标识一条数据通信链路中一端的概念,用于数据的发送和接收。根据端口的用途和范围,可以将端口分为众所周知端口、注册端口和动态端口三种类型。了解不同端口类型的作用和使用方法,可以帮助我们更好地理解网络通信的过程和应用程序的工作原理。
三、网络模型


四、协议





五、网络通信的过程

首先,上层协议将数据传递给下层协议,上层协议称为“数据的应用层”或“上层协议”,下层协议称为“数据的传输层”或“下层协议”。
下层协议会将上层数据封装成自己的数据格式,通常包括一个协议头和一个协议尾。
协议头通常包括一些元数据,例如源IP地址、目的IP地址、协议类型等信息,协议尾通常包括一些用于检测数据完整性和错误检测的校验和等信息。
封装完成后,下层协议将生成的数据包传递给下一层协议,下一层协议再将该数据包封装成自己的格式,以此类推,直到数据包被传输到目的地。
在接收端,各层协议会解封数据包,将数据包还原为原始数据,并将数据传递给上一层协议进行处理。
总之,封装是将一个协议的数据添加到另一个协议的数据中,形成一个新的数据包的过程。封装是计算机网络中实现数据传输的重要方法,通过封装,不同层次之间的数据可以进行传输和交换,从而实现了网络通信。

接收端主机从网络中接收到一个数据包,数据包中包含了多个数据流。
接收端主机的网络协议栈首先对数据包进行解封装,将数据包还原为原始数据流。
然后,接收端主机的网络协议栈会根据数据包头部中的端口号信息,分别将数据流交给对应的应用程序处理。这个过程称为demultiplexing。
在该过程中,每个应用程序都会注册一个或多个端口号,接收端主机的网络协议栈会根据数据包头部中的端口号信息,将数据流分别发送给对应的应用程序。
如果数据包头部中的端口号在接收端主机中没有被注册,数据包将被丢弃。
总之,demultiplexing是将一个数据包中的多个数据流进行分离的过程。它是计算机网络中实现多路复用的重要技术,可以将多个应用程序的数据流通过一个网络连接进行传输,从而提高网络传输的效率和灵活性。

六、socket 介绍

Socket是一种通用的通信机制,它可以在不同的操作系统和平台上使用,包括Linux、Windows、Unix等。
Socket通信是基于TCP/IP协议的,它提供了可靠的数据传输和错误处理机制。
Socket通信是面向连接的,它需要先建立连接,然后才能进行数据传输。建立连接是通过“三次握手”协议实现的。
Socket通信提供了两种类型的套接字:流式套接字和数据报套接字。流式套接字提供了面向连接的数据传输,数据报套接字则提供了无连接的数据传输。
在使用Socket进行通信时,需要指定目标IP地址和端口号,通过这些信息可以建立连接,并进行数据传输和通信。
总之,Socket是计算机网络中一种通信机制,它可以在不同的操作系统和平台上使用,提供了可靠的数据传输和错误处理机制。Socket通信是基于TCP/IP协议的,它需要先建立连接,然后才能进行数据传输。Socket通信提供了两种类型的套接字,可以根据需要选择合适的套接字类型进行数据传输和通信。
七、字节序

大端字节序:在大端字节序中,高位字节保存在内存的低地址中,低位字节保存在内存的高地址中。例如,整数0x12345678在内存中的排列顺序为0x12 0x34 0x56 0x78。
小端字节序:在小端字节序中,低位字节保存在内存的低地址中,高位字节保存在内存的高地址中。例如,整数0x12345678在内存中的排列顺序为0x78 0x56 0x34 0x12。
在网络通信中,由于不同的计算机体系结构和操作系统采用的字节序不同,因此需要进行字节序的转换。通常使用网络字节序(也称为大端字节序)来进行数据传输和通信,接收端在接收到数据后需要将数据从网络字节序转换为本地字节序,即进行字节序的转换。
总之,字节序是指在多字节数据中字节的排列顺序,通常有大端字节序和小端字节序两种。在进行数据交换和通信时,需要进行字节序的转换。在网络通信中,通常使用网络字节序进行数据传输和通信,接收端在接收到数据后需要进行字节序的转换。
八、字节序转换函数

htons()函数:将本地字节序转换为网络字节序。htons()函数接受一个16位整数作为参数,返回一个网络字节序的16位整数。例如,short port = htons(8080);。
ntohs()函数:将网络字节序转换为本地字节序。ntohs()函数接受一个16位整数作为参数,返回一个本地字节序的16位整数。例如,short port = ntohs(0x7a69);。
htonl()函数:将本地字节序转换为网络字节序。htonl()函数接受一个32位整数作为参数,返回一个网络字节序的32位整数。例如,int ip = htonl(0x0a000001);。
ntohl()函数:将网络字节序转换为本地字节序。ntohl()函数接受一个32位整数作为参数,返回一个本地字节序的32位整数。例如,int ip = ntohl(0x0100007f);。
这些函数通常在网络编程中使用,用于将数据从本地字节序转换为网络字节序或从网络字节序转换为本地字节序,从而实现跨平台的数据传输和通信。
九、socket 地址

通用socket地址包含了协议族、IP地址、端口号等信息,不同的协议族和地址类型对应的通用socket地址结构体可能会有所不同。在C语言中,通用socket地址通常表示为sockaddr结构体,其定义如下:
struct sockaddr {
unsigned short sa_family; // 协议族
char sa_data[14]; // 地址信息
};
其中,sa_family表示协议族,可以是AF_INET(IPv4协议族)、AF_INET6(IPv6协议族)等等;sa_data表示地址信息,该字段的长度取决于具体的协议族和地址类型。
通用socket地址通常会和其他的函数一起使用,例如bind()、connect()、accept()等函数,用于指定网络地址和端口号。在使用时,通常需要将通用socket地址强制转换为特定的协议族和地址类型,例如将sockaddr结构体转换为sockaddr_in结构体(用于IPv4地址)或sockaddr_in6结构体(用于IPv6地址)等。
总之,通用socket地址是一种表示通用网络地址的结构体,可以用于不同的协议族和地址类型。通用socket地址常用于网络编程中,用于指定网络地址和端口号。在使用时,需要将通用socket地址强制转换为特定的协议族和地址类型。

在网络编程中,常用的专用socket地址包括sockaddr_in结构体(用于IPv4地址)和sockaddr_in6结构体(用于IPv6地址)。这些结构体包含了协议族、IP地址、端口号等信息,用于指定一个特定的网络地址。
sockaddr_in结构体定义如下:
struct sockaddr_in {
short sin_family; // 协议族
unsigned short sin_port; // 端口号
struct in_addr sin_addr; // IPv4地址
char sin_zero[8]; // 未使用
};
其中,sin_family表示协议族,固定为AF_INET;sin_port表示端口号,采用网络字节序(使用htons()函数进行转换);sin_addr表示IPv4地址,使用struct in_addr结构体表示;sin_zero为未使用的8个字节。
sockaddr_in6结构体定义如下:
struct sockaddr_in6 {
short sin6_family; // 协议族
unsigned short sin6_port; // 端口号
unsigned int sin6_flowinfo; // 传输控制信息
struct in6_addr sin6_addr; // IPv6地址
unsigned int sin6_scope_id; // 地址作用域
};
其中,sin6_family表示协议族,固定为AF_INET6;sin6_port表示端口号,采用网络字节序;sin6_flowinfo表示传输控制信息;sin6_addr表示IPv6地址,使用struct in6_addr结构体表示;sin6_scope_id表示地址作用域。
总之,专用socket地址是一种特定协议族和地址类型的地址结构,用于表示一个特定的网络地址。常见的专用socket地址包括sockaddr_in结构体(用于IPv4地址)和sockaddr_in6结构体(用于IPv6地址)。这些结构体包含了
十、IP 地址转换函数

在网络编程中,经常需要将字符串形式的IP地址转换为整数形式的IP地址,或者将整数形式的IP地址转换为字符串形式的IP地址。这可以通过使用inet_addr()和inet_ntoa()函数来实现。
inet_addr()函数将一个字符串形式的IP地址转换为一个32位的网络字节序的整数形式的IP地址,函数原型如下:
unsigned long inet_addr(const char *cp);
例如,下面的代码将字符串形式的IP地址"192.168.1.1"转换为整数形式的IP地址:
unsigned long ip = inet_addr("192.168.1.1");
inet_ntoa()函数将一个32位的网络字节序的整数形式的IP地址转换为一个字符串形式的IP地址,函数原型如下:
char *inet_ntoa(struct in_addr in);
其中,in_addr是一个结构体,表示一个32位的网络字节序的整数形式的IP地址。例如,下面的代码将整数形式的IP地址转换为字符串形式的IP地址:
struct in_addr addr;
addr.s_addr = ip;
char *ip_str = inet_ntoa(addr);
主机字节序和网络字节序的转换
在网络通信中,通常使用网络字节序进行数据传输和通信,而在计算机内部使用的是主机字节序。因此,在进行数据传输和通信时,需要进行主机字节序和网络字节序之间的转换。这可以通过使用htons()、ntohs()、htonl()和ntohl()等函数来实现。
htons()函数将一个16位的主机字节序的整数转换为网络字节序的整数,函数原型如下:
uint16_t htons(uint16_t hostshort);
例如,下面的代码将一个16位的主机字节序的整数转换为网络字节序的整数:
uint16_t host_num = 12345;
uint16_t net_num = htons(host_num);
ntohs()函数将一个16位的网络字节序的整数转换为主机字节序的整数,函数原型如下:
uint16_t ntohs(uint16_t netshort);
例如,下面的代码将一个16位的网络字节序的整数转换为主机字节序的整数:
uint16_t net_num = 0x3039;
uint16_t host_num = ntohs(net_num);
htonl()函数将一个32位的主机字节序的整数转换为网络字节序的整数,函数原型如下:
uint32_t htonl(uint32_t hostlong);
例如,下面的代码将一个32位的主机字节序的整数转换为网络字节序的整数:
uint32_t host_num = 0x01020304;
uint32_t net_num = htonl(host_num);
ntohl()函数将一个32位的网络字节序的整数转换为主机字节序的整数,函数原型如下:
uint32_t ntohl(uint32_t netlong);
例如,下面的代码将一个32位的网络字节序的整数转换为主机字节序的整数:
uint32_t net_num = 0x04030201;
uint32_t host_num = ntohl(net_num);
总之,IP地址转换包括字符串IP地址转整数IP地址和整数IP地址转字符串IP地址两种方式,可以通过inet_addr()和inet_ntoa()函数来实现。主机字节序和网络字节序的转换可以通过htons()、ntohs()、htonl()和ntohl()等函数来实现。在进行网络通信时,需要进行主机字节序和网络字节序之间的转换,以保证数据的正确传输。
十一、TCP 通信流程

创建Socket:服务端和客户端都需要创建一个Socket对象,通过该对象进行通信。在创建Socket时,需要指定协议族(一般为AF_INET或AF_INET6)、传输层协议(一般为SOCK_STREAM)、以及协议编号(一般为0)等参数。
绑定Socket:服务端需要将Socket绑定到一个固定的IP地址和端口号上。这可以通过bind()函数实现。如果绑定成功,则表明服务端已经可以接收客户端的连接请求。
监听连接请求:服务端在绑定成功后,可以调用listen()函数开始监听客户端的连接请求。listen()函数的参数是一个整数值,表示在等待连接队列中最多可以容纳的连接数。
接受连接请求:一旦服务端开始监听连接请求,客户端就可以通过connect()函数向服务端发送连接请求。服务端在接收到连接请求后,可以调用accept()函数接受该连接请求,并返回一个新的Socket对象,用于与客户端进行通信。
数据传输:一旦服务端和客户端建立起连接,它们就可以通过Socket对象进行数据传输。服务端和客户端都可以调用send()函数和recv()函数进行数据发送和接收。
关闭连接:数据传输完成后,服务端和客户端都可以调用close()函数关闭连接。在关闭连接之前,服务端和客户端都应该通过shutdown()函数发送一个关闭信号,以确保对方可以正常地收到该信号并完成数据传输。
需要注意的是,服务端和客户端的通信流程可能会因为实际场景的不同而有所差异,例如在使用多线程或多进程时,服务端需要在accept()函数中调用fork()或pthread_create()来创建新的进程或线程来处理新的连接请求。
十二、socket 函数

socket()
函数原型:int socket(int domain, int type, int protocol)
作用:创建socket套接字。
参数:
domain:协议族,常见的有AF_INET(IPv4)和AF_INET6(IPv6)。
type:套接字类型,常见的有SOCK_STREAM(流套接字)和SOCK_DGRAM(数据报套接字)。
protocol:协议编号,常见的有IPPROTO_TCP(TCP协议)和IPPROTO_UDP(UDP协议)。
返回值:成功返回一个新的socket的文件描述符(socket descriptor),失败返回-1。
bind()
函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
作用:将socket与本地的IP地址和端口号绑定。
参数:
sockfd:socket套接字的文件描述符。
addr:指向sockaddr结构体的指针,包含了IP地址和端口号信息。
addrlen:sockaddr结构体的长度。
返回值:成功返回0,失败返回-1。
listen()
函数原型:int listen(int sockfd, int backlog)
作用:将socket设置为监听状态,等待客户端的连接请求。
参数:
sockfd:socket套接字的文件描述符。
backlog:等待连接队列的最大长度。
返回值:成功返回0,失败返回-1。
accept()
函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
作用:接受客户端的连接请求,并返回一个新的socket套接字,用于与客户端进行通信。
参数:
sockfd:socket套接字的文件描述符。
addr:指向sockaddr结构体的指针,用于存储客户端的IP地址和端口号信息。
addrlen:sockaddr结构体的长度。
返回值:成功返回一个新的socket的文件描述符,失败返回-1。
connect()
函数原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
作用:向指定的IP地址和端口号发起连接请求。
参数:
sockfd:socket套接字的文件描述符。
addr:指向sockaddr结构体的指针,包含了服务端的IP地址和端口号信息。
addrlen:sockaddr结构体的长度。
返回值:成功返回0,失败返回-1。
write() / send()和read() / recv()
函数原型:
ssize_t write(int fd, const void *buf, size_t count)
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
ssize_t read(int fd, void *buf, size_t count)
ssize_t recv(int sockfd, void *buf, size_t len, int flags)
作用:用于发送和接收数据。
参数:
fd/sockfd:文件描述符或socket的文件描述符。
buf:指向要发送或接收的数据的缓冲区。
count/len:发送或接收的数据的字节数。
flags:发送或接收数据的选项,默认为0。
返回值:成功返回发送或接收的字节数,失败返回-1。
总的来说,socket()、bind()、listen()、accept()、connect()、write()和read()是socket编程中最常用的一些函数,通过它们可以实现网络通信的基本功能。
十三、TCP通信实现(服务器端)

十四、TCP通信实现(客户端)
1.创建socket对象,使用socket()函数创建一个socket对象,并指定协议族、传输类型和协议编号。
2.连接服务器,使用connect()函数向服务器发起连接请求,指定服务器的IP地址和端口号。
3.与服务器进行通信,使用新得到的socket对象与服务器进行通信,可以使用write()函数向服务器发送数据,使用read()函数从服务器接收数据。
4.关闭socket对象,在通信结束后,需要使用close()函数关闭socket对象。
十五、TCP三次握手

十六、滑动窗口

具体来说,TCP的滑动窗口是由发送方和接收方各自维护的一个窗口缓存区。发送方通过维护一个发送窗口来控制发送数据的速率,接收方通过维护一个接收窗口来告知发送方自己可以接收的数据的大小。发送方每次发送数据时,会根据接收方返回的窗口大小来动态调整自己的发送窗口大小,以保证在网络拥塞的情况下不会发送过多的数据,从而导致网络阻塞或数据丢失。
在TCP的滑动窗口中,发送方和接收方都维护一个窗口大小变量和一个窗口指针变量。发送方的窗口大小表示自己可以发送的数据量,窗口指针指向下一个可以发送的数据,接收方的窗口大小表示自己可以接收的数据量,窗口指针指向下一个期望接收的数据。
发送方在发送数据时,会将数据按照窗口大小分成多个数据块,每发送一个数据块,就将发送窗口大小减去对应数据块的大小。接收方在接收数据时,会根据接收到的数据更新接收窗口大小和窗口指针,然后将窗口大小和窗口指针发送给发送方,以告知发送方自己可以接收的数据量和下一个期望接收的数据。
发送方收到接收方返回的窗口大小后,会根据窗口大小调整自己的发送窗口大小,然后将窗口指针更新到下一个可以发送的数据块的位置。这样,发送方就可以根据接收方返回的窗口大小和窗口指针来动态调整发送数据的速率,以适应网络状况的变化。
总的来说,TCP的滑动窗口是一种非常重要的流量控制机制,它可以根据网络状况的变化动态调整发送和接收数据的速率,从而保证数据传输的可靠性和效率。
十七、TCP四次挥手

十八、多进程实现并发服务器

十九、多线程实现并发服务器

二十、TCP状态转换

二十一、半关闭、端口复用

半关闭可以在以下情况下使用:
1.应用程序只需要向远程主机发送数据,而不需要再接收数据时,可以关闭套接字上的写端(写半关闭)。
2.应用程序只需要接收远程主机发送的数据,而不需要再向远程主机发送数据时,可以关闭套接字上的读端(读半关闭)。
半关闭的实现可以通过调用 shutdown() 函数来完成。例如,如果要写半关闭一个套接字,可以使用以下代码:
shutdown(sock_fd, SHUT_WR);
其中,sock_fd 是套接字文件描述符,SHUT_WR 表示写半关闭。
总之,半关闭是一种在网络编程中常见的技术,它可以提高网络连接的灵活性和可靠性,但需要应用程序和网络协议的支持。

在 Linux 中,端口复用可以通过设置套接字选项来实现。具体而言,可以使用 setsockopt() 函数设置 SO_REUSEADDR 或者 SO_REUSEPORT 套接字选项来开启端口复用功能。
SO_REUSEADDR 选项
SO_REUSEADDR 选项可用于在一个套接字关闭后立即释放其端口,以便其他套接字可以立即绑定到该端口上。如果没有设置 SO_REUSEADDR 选项,那么在套接字关闭后,操作系统会将该端口保留一段时间,以确保不会有任何延迟数据包到达。这段时间称为 TIME_WAIT 状态,此时其他套接字不能立即绑定到该端口上。
在设置 SO_REUSEADDR 选项时,需要将其设置为一个非零值。例如:
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
其中 sockfd 是套接字文件描述符。
SO_REUSEPORT 选项
SO_REUSEPORT 选项可用于在同一 IP 地址和端口上启动多个套接字监听。如果没有设置 SO_REUSEPORT 选项,那么只能有一个套接字绑定到同一 IP 地址和端口上。在设置 SO_REUSEPORT 选项时,需要将其设置为一个非零值。例如:
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse));
需要注意的是,SO_REUSEPORT 选项只在 Linux 3.9 及以上版本中可用。
总之,端口复用是一种非常有用的技术,它可以提高网络应用程序的灵活性和可靠性。
端口复用技术可以让多个套接字同时绑定到同一个端口,这样可以实现多个进程或应用程序共享同一个端口,避免了端口被占用而导致启动失败的情况。但是,多个套接字同时绑定到同一个端口也会带来一些影响,主要包括以下几点:
数据分发问题:当多个套接字同时绑定到同一个端口时,如果有数据传输到这个端口,那么操作系统会将数据复制到所有绑定到这个端口的套接字中。这可能会导致数据分发不均,某些套接字可能会接收到更多的数据,而某些套接字可能会接收到较少的数据。
进程之间的通信:如果有多个进程或应用程序同时绑定到同一个端口,那么它们之间可能会出现通信问题。因为这些进程或应用程序是独立的,它们无法共享数据,也无法协调数据的接收和处理。
端口复用相关的选项:如果多个套接字同时绑定到同一个端口,那么它们之间可能会相互影响,例如SO_REUSEADDR和SO_REUSEPORT选项等。在这种情况下,需要仔细考虑这些选项的使用,并确保它们不会导致意外的行为。
综上所述,虽然端口复用技术可以让多个套接字同时绑定到同一个端口,但是需要注意上述问题,并根据实际情况进行选择和使用。
二十二、IO多路复用(多路转接)简介
I/O多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux下实现I/O多路复用的系统调用主要有select、poll和epoll。

在阻塞等待 BIO 模型中,应用程序通过调用阻塞 I/O 函数来等待数据的到达。例如,使用阻塞等待 BIO 模型实现 TCP 服务器时,可以使用 accept() 函数阻塞等待客户端连接。如果没有客户端连接到达,accept() 函数将一直阻塞,直到有新的连接到达为止。

在非阻塞 NIO 模型中,应用程序可以通过以下步骤来实现异步 I/O 操作:
创建非阻塞套接字(non-blocking socket),并将其设置为非阻塞模式。在 Linux 中,可以使用 fcntl() 函数将套接字设置为非阻塞模式。
使用 select()、poll()、epoll() 等多路复用函数来等待 I/O 事件的发生。这些函数可以监视多个套接字的 I/O 事件,并在有事件发生时立即返回。在 Linux 中,epoll() 是一种高效的多路复用函数,可以同时监视多个套接字,具有较高的性能和可扩展性。
当有 I/O 事件发生时,应用程序可以通过调用非阻塞 I/O 函数来进行 I/O 操作。在 Linux 中,可以使用 read()、write()、recv()、send() 等非阻塞 I/O 函数来实现。非阻塞 NIO 模型是一种高效、可扩展的网络编程模型,适用于高并发、高吞吐量的网络应用程序。但是,使用非阻塞 NIO 模型需要更多的编程工作,因为应用程序需要手动管理 I/O 事件和套接字的状态。
二十三、select API介绍

nfds:待检查的最大文件描述符值加 1。
readfds:包含待检查的可读文件描述符的文件描述符集合。
writefds:包含待检查的可写文件描述符的文件描述符集合。
exceptfds:包含待检查的异常文件描述符的文件描述符集合。
timeout:等待超时时间的指针,如果为 NULL,则表示一直等待,直到有事件发生。
返回值说明:
如果有文件描述符在指定时间内发生了可读、可写或异常状态,则返回一个大于 0 的值,表示就绪的文件描述符的个数。
如果在指定时间内没有任何文件描述符发生可读、可写或异常状态,则返回 0。
如果函数调用失败,则返回 -1,并设置 errno。
使用 select() 函数的步骤如下:
创建一组文件描述符集合,并将待监视的文件描述符添加到对应的集合中。
调用 select() 函数,并传入待监视的文件描述符集合、超时时间等参数。
如果 select() 函数返回大于 0 的值,则表示有文件描述符发生了可读、可写或异常状态。
使用 FD_ISSET() 宏函数来遍历就绪的文件描述符集合,检查哪些文件描述符发生了可读、可写或异常状态,并进行相应的处理。

FD_SET() 宏用于将一个文件描述符添加到 fd_set 结构体中。参数 fd 表示待添加的文件描述符,参数 set 是一个指向 fd_set 结构体的指针,表示待添加文件描述符的集合。使用 FD_SET() 宏可以将一个文件描述符添加到文件描述符集合中,以便调用 select() 函数对其进行监视。FD_CLR() 宏用于将一个文件描述符从 fd_set 结构体中删除。参数 fd 表示待删除的文件描述符,参数 set 是一个指向 fd_set 结构体的指针,表示待删除文件描述符的集合。使用 FD_CLR() 宏可以将一个文件描述符从文件描述符集合中删除,以便在处理完该文件描述符后,避免重复处理已关闭的文件描述符。FD_ISSET() 宏用于判断一个文件描述符是否在 fd_set 结构体中。参数 fd 表示待判断的文件描述符,参数 set 是一个指向 fd_set 结构体的指针,表示待判断文件描述符的集合。使用 FD_ISSET() 宏可以判断一个文件描述符是否在文件描述符集合中,以便确定该文件描述符是否处于就绪状态,需要进行处理。
这四个宏通常与 select() 函数一起使用,用于构建多路复用 I/O 模型。在使用 select() 函数前,需要先创建一个 fd_set 结构体,并使用 FD_ZERO() 宏将其清零,然后将待监视的文件描述符添加到集合中,使用 select() 函数等待文件描述符就绪,最后使用 FD_ISSET() 宏判断文件描述符是否就绪,并使用 FD_CLR() 宏将其从集合中删除,以便下次调用 select() 函数时重新添加到集合中。

创建 fd_set 结构体并添加待监视的文件描述符
在调用 select() 函数前,需要先创建一个 fd_set 结构体,并使用 FD_ZERO() 宏将其清零,然后将待监视的文件描述符添加到集合中,使用 FD_SET() 宏将其加入到集合中。
调用 select() 函数
调用 select() 函数等待文件描述符就绪,当文件描述符就绪时,select() 函数返回,程序继续执行。参数 nfds 指定待监视的文件描述符集合中的最大文件描述符值加 1,readfds、writefds、exceptfds 分别为指向待监视的读、写、异常文件描述符集合的指针,timeout 表示等待时间,如果为 NULL 则表示一直等待。
使用 FD_ISSET() 宏判断文件描述符是否就绪
select() 函数返回后,需要使用 FD_ISSET() 宏判断哪些文件描述符已经就绪,以便进行相应的操作。如果文件描述符已经就绪,FD_ISSET() 宏返回真,否则返回假。
使用 FD_CLR() 宏将已处理的文件描述符从集合中删除
在处理完一个文件描述符后,需要使用 FD_CLR() 宏将其从文件描述符集合中删除,以便下次调用 select() 函数时重新添加到集合中。
在多数情况下,select() 函数会被用于实现基于事件驱动的 I/O 模型。当有 I/O 事件发生时,select() 函数会返回就绪的文件描述符,然后程序可以根据文件描述符进行相应的处理,例如读取数据、写入数据等。使用 select() 函数可以避免阻塞等待单个 I/O 事件发生,提高程序的并发性和响应性。
二十四、poll API介绍

文件描述符数量限制
在一些操作系统中,select() 函数对文件描述符的数量有限制,通常最多只能监视 1024 个文件描述符。如果需要监视更多的文件描述符,可以使用 poll() 函数或 epoll API。
慢速系统调用
select() 函数是一个慢速系统调用,即在调用 select() 函数时会阻塞当前进程,直到有文件描述符就绪或等待超时。这会导致程序的响应性变差,特别是当需要监视大量的文件描述符时。
频繁的内存拷贝
在调用 select() 函数时,需要将文件描述符集合从用户态拷贝到内核态,然后将就绪的文件描述符集合从内核态拷贝回用户态。这会导致频繁的内存拷贝,降低程序的性能。
没有事件通知机制
在使用 select() 函数时,需要不断地调用它来检查文件描述符是否就绪,这会导致 CPU 的大量消耗。而且 select() 函数并没有事件通知机制,即当文件描述符就绪时,它并不能主动通知应用程序,而是需要应用程序不断地轮询。
不支持对文件描述符的动态增删
在使用 select() 函数时,如果需要增加或删除待监视的文件描述符,需要重新创建一个新的文件描述符集合,并将待监视的文件描述符重新添加到集合中。这会导致程序的复杂性增加,特别是当需要动态地增加或删除文件描述符时。
综上所述,select() 函数在某些情况下存在一些缺点,特别是在需要监视大量文件描述符或需要动态增删文件描述符时。为了解决这些问题,可以使用其他的 I/O 多路复用函数,如 poll() 函数或 epoll API。

二十五、epoll API介绍

使用 epoll() 多路复用机制的基本流程如下:
创建 epoll 实例,即调用 epoll_create() 函数,该函数返回一个文件描述符,用于后续的 epoll 操作。
向 epoll 实例中添加待监视的文件描述符,即调用 epoll_ctl() 函数,指定操作类型为 EPOLL_CTL_ADD,参数 fd 为待监视的文件描述符,参数 event 为待监视的事件类型和数据。
等待文件描述符就绪,即调用 epoll_wait() 函数,该函数会一直阻塞,直到有文件描述符就绪或超时,然后返回就绪的文件描述符列表和事件类型。
处理就绪的文件描述符和事件,即根据返回的文件描述符列表和事件类型,处理相应的 I/O 操作,例如读取数据、写入数据等。
如果需要修改或删除待监视的文件描述符,可以调用 epoll_ctl() 函数,指定操作类型为 EPOLL_CTL_MOD 或 EPOLL_CTL_DEL,然后重新调用 epoll_wait() 等待文件描述符就绪。
使用 epoll() 多路复用机制可以避免使用多个线程或进程处理多个文件描述符,从而提高程序的并发性能和可维护性。同时,epoll() 多路复用机制也具有较低的系统开销和较高的效率,适用于高并发的网络编程场景。

epoll_create()
epoll_create() 用于创建一个 epoll 实例,并返回一个文件描述符,该文件描述符可以用于后续的 epoll 操作。epoll_create() 的原型定义如下:
int epoll_create(int size);
参数 size 指定 epoll 实例中可以监视的文件描述符数量,实际上该参数在 Linux 2.6.8 之后已经被忽略,可以传递任何值。
epoll_ctl()
epoll_ctl() 用于向 epoll 实例中添加、修改或删除待监视的文件描述符。epoll_ctl() 的原型定义如下:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数 epfd 是 epoll 实例的文件描述符,op 是操作类型,可以是 EPOLL_CTL_ADD、EPOLL_CTL_MOD 或 EPOLL_CTL_DEL,分别表示添加、修改和删除操作。参数 fd 是待监视的文件描述符,event 是一个 epoll_event 结构体,用于指定待监视的事件类型和数据。
epoll_event 结构体的定义如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; // 监视的事件类型(例如 EPOLLIN、EPOLLOUT、EPOLLERR 等)
epoll_data_t data; // 用户数据(例如文件描述符、指针等)
};
epoll_wait()
epoll_wait() 用于在 epoll 实例上等待文件描述符就绪,并返回就绪的文件描述符列表。epoll_wait() 的原型定义如下:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数 epfd 是 epoll 实例的文件描述符,events 是一个 epoll_event 结构体数组,用于存储就绪的文件描述符和事件类型。参数 maxevents 指定 events 数组的长度,即最多可以返回多少个就绪的文件描述符。参数 timeout 指定等待时间,如果为 -1 则表示一直等待,如果为 0 则表示立即返回,如果大于 0 则表示等待指定的毫秒数。
epoll API 支持边缘触发(ET)和水平触发(LT)两种工作模式,可以根据具体的应用场景选择合适的模式。与 select() 和 poll() 函数相比,epoll API 具有更高效、更灵活的特点,可以监视大量的文件描述符,并且支持边缘触发和水平触发模式。
二十六、epoll的两种工作模式

边缘触发(ET)
在边缘触发模式下,当文件描述符上有新的事件发生时,epoll_wait() 函数会返回该事件,然后需要立即处理该事件,否则下次调用 epoll_wait() 函数时不会再次返回该事件。特别地,对于可读和可写事件,只有在文件描述符状态从不可读/不可写转变为可读/可写时才会触发该事件。
边缘触发模式相对于水平触发模式有更高的效率,因为它只在事件发生时通知应用程序,减少了无用的通知。但是,它也需要应用程序具有更高的处理速度,以避免事件丢失。
水平触发(LT)
在水平触发模式下,当文件描述符上有新的事件发生时,epoll_wait() 函数会返回该事件,然后需要一直处理该事件,直到文件描述符上的事件被处理完毕或不再有新的事件发生为止。对于可读和可写事件,只要文件描述符上还有数据可读/可写,就会一直触发该事件。
水平触发模式需要应用程序一直处理文件描述符上的事件,否则会导致 CPU 的大量消耗。但是,它也更加灵活,可以在任何时候处理文件描述符上的事件,而不必担心事件丢失。
综上所述,边缘触发模式和水平触发模式各有优缺点,应根据具体的应用场景选择合适的模式。如果需要高效地处理事件,可以选择边缘触发模式;如果需要更加灵活地处理事件,并且可以承受更高的 CPU 消耗,可以选择水平触发模式。
*epoll高效的原因
使用红黑树数据结构:epoll 将待监视的文件描述符存储在红黑树中,并使用哈希表来快速查找文件描述符对应的数据结构,从而使得监视文件描述符的查找效率更高。
支持边缘触发和水平触发模式:epoll 可以选择边缘触发(ET)和水平触发(LT)两种模式,可以根据具体的应用场景选择合适的模式,进一步提高程序的效率和性能。
避免大量的系统调用:epoll 可以在一个系统调用中同时监视多个文件描述符,避免了传统的 select() 和 poll() 函数中需要多次调用的问题,从而减少了系统调用的次数,提高了程序的效率。
避免了文件描述符集合的限制:epoll 没有像 select() 和 poll() 函数那样限制监视的文件描述符数量,可以处理大量的文件描述符,从而适用于高并发的网络编程场景。
支持文件描述符的复用:epoll 可以通过设置文件描述符的属性,使其在多个 epoll 实例中共享,从而避免了文件描述符重复创建的问题,提高了程序的效率和可维护性。
二十七、UDP通信实现

在客户端代码中,首先创建UDP Socket,并设置服务端的IP地址和端口号。然后,使用sendto函数向服务端发送消息,并使用recvfrom函数接收服务端发送的消息。最后,关闭Socket。
需要注意的是,在UDP通信中,由于UDP协议不保证数据的可靠性和顺序性,因此需要在应用程序中自行实现数据的校验和处理。此外,UDP协议通常用于实时性比较高的应用场景,例如视频和音频传输等。
二十八、广播

setsockopt:setsockopt是一个Socket API函数,用于设置Socket的选项。通过setsockopt函数,可以配置Socket的多种参数,例如超时时间,缓存大小,广播选项等。setsockopt函数的原型如下:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
其中,sockfd是Socket的文件描述符,level表示选项所在的协议层,optname表示选项的名称,optval表示选项的值,optlen表示选项值的长度。例如,可以使用setsockopt函数来设置SO_BROADCAST选项,以启用广播模式:
int broadcastEnable = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, sizeof(broadcastEnable));
上述代码中,sockfd是Socket的文件描述符,SOL_SOCKET表示Socket本身的选项,SO_BROADCAST表示广播选项,broadcastEnable是一个int类型的变量,设置为1表示启用广播模式。通过调用setsockopt函数,可以将Socket设置为广播模式,从而实现向同一网络中的所有设备发送消息的功能。
二十九、组播

在IPv4中,组播地址的范围是224.0.0.0到239.255.255.255,其中224.0.0.0是预留地址,不能用于实际通信,239.255.255.255是全局组播地址,用于向整个Internet发送消息。在IPv6中,组播地址的范围是ff00::/8,其中ff01::是节点本地组播地址,ff02::是本地链路组播地址,ff05::是站点本地组播地址,ff08::是组织本地组播地址。
在Linux中,可以使用Socket API中的setsockopt函数来加入和退出组播,需要设置IPPROTO_IP或IPPROTO_IPV6协议的IP_ADD_MEMBERSHIP或IP_DROP_MEMBERSHIP选项。例如,可以使用以下代码将Socket加入到组播地址组中:
// 加入IPv4组播地址组
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1"); // 组播地址
mreq.imr_interface.s_addr = htonl(INADDR_ANY); // 本地IP地址
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
// 加入IPv6组播地址组
struct ipv6_mreq mreq6;
inet_pton(AF_INET6, "ff02::1", &mreq6.ipv6mr_multiaddr); // 组播地址
mreq6.ipv6mr_interface = 0; // 本地接口
setsockopt(sockfd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &mreq6, sizeof(mreq6));
上述代码中,sockfd是Socket的文件描述符,IPPROTO_IP和IPPROTO_IPV6分别表示IPv4和IPv6协议,IP_ADD_MEMBERSHIP和IPV6_ADD_MEMBERSHIP表示加入组播的选项,mreq和mreq6是组播地址和本地接口的结构体。通过调用setsockopt函数,可以将Socket加入到指定的组播地址组中,从而实现组播通信功能。

在客户端代码中,同样首先创建Socket,然后设置Socket的地址和端口号。然后,通过循环调用fgets函数,从标准输入中获取用户输入的消息,并通过sendto函数将消息发送给服务端。客户端不需要加入组播地址组,只需要向服务端发送消息即可。
需要注意的是,服务端和客户端需要使用相同的组播地址和端口号,才能进行组播通信。此外,服务端需要在运行前,先运行ifconfig命令,获取本机的IP地址,并将其设置为IMR_INTERFACE选项的值。
三十、本地套接字通信

在使用本地套接字通信时,需要指定一个本地套接字文件(Socket File),用于标识通信的进程。本地套接字文件储存在文件系统中,并具有与普通文件相同的权限和属性。本地套接字文件的命名规则为以NUL结尾的路径名(路径名中不能包含斜杠),通常存放在/tmp目录中。