网络的介绍
网络的历史和协议
网络的历史
诞生初期(20 世纪 60 年代):美国国防部高级研究计划局(ARPA)为了应对冷战时期的军事需求,开发了 ARPANET,这是世界上第一个分组交换网络,标志着计算机网络的诞生。它采用了分布式的网络结构,通过将数据分成小的数据包进行传输,提高了网络的可靠性和灵活性。
发展阶段(20 世纪 70 - 80 年代):随着计算机技术的不断发展,局域网(LAN)开始兴起。以太网技术逐渐成熟,它采用了载波监听多路访问 / 冲突检测(CSMA/CD)机制,使得多个计算机能够在同一网络中共享通信介质。同时,网络协议也在不断丰富,如 TCP/IP 协议族逐渐形成。
普及阶段(20 世纪 90 年代):互联网在这一时期得到了广泛的普及和应用。万维网(WWW)的出现使得信息的传播和共享变得更加便捷,用户可以通过浏览器访问各种网站,获取丰富的信息资源。同时,网络技术也在不断创新,如宽带网络技术的发展,提高了网络的传输速度和带宽。
高速发展阶段(21 世纪以来):进入 21 世纪,网络技术迎来了高速发展的时期。无线网络技术日益成熟,如 Wi-Fi、蓝牙等,使得人们可以随时随地接入网络。物联网(IoT)技术的兴起,将各种设备连接到网络中,实现了设备之间的互联互通。同时,云计算、大数据等技术的发展,也为网络的应用提供了更广阔的空间。
网络协议
网络层协议
IP(Internet Protocol)协议:是互联网的核心协议,负责将数据包从源主机传输到目的主机。它定义了数据包的格式和寻址方式,通过 IP 地址来标识网络中的设备。
ICMP(Internet Control Message Protocol)协议:用于在网络设备之间传递控制消息,如错误报告、网络状态查询等。它可以帮助网络管理员诊断网络故障,提高网络的可靠性。
IGMP(Internet Group Management Protocol)协议:主要用于管理多播组,允许主机向路由器报告其加入或离开多播组的信息,实现高效的多播数据传输。
传输层协议
TCP(Transmission Control Protocol)协议:是一种面向连接的、可靠的传输协议。它在发送数据之前,会先与对方建立连接,然后将数据分成多个数据包进行传输,并通过确认、重传等机制保证数据的可靠传输。适用于对数据准确性要求较高的应用,如文件传输、电子邮件等。
UDP(User Datagram Protocol)协议:是一种无连接的、不可靠的传输协议。它不建立连接,直接将数据包发送出去,速度快,但不保证数据的可靠传输。适用于对实时性要求较高的应用,如视频会议、在线游戏等。
应用层协议
HTTP(HyperText Transfer Protocol)协议:是万维网应用的基础协议,用于在客户端和服务器之间传输超文本信息,如网页、图片、视频等。它采用请求 - 响应模型,客户端发送请求,服务器返回响应。
SMTP(Simple Mail Transfer Protocol)协议:用于发送电子邮件,负责将邮件从发件人的邮件服务器传输到收件人的邮件服务器。
POP3(Post Office Protocol - Version 3)协议:用于接收电子邮件,允许用户从邮件服务器上下载邮件到本地客户端。
DNS(Domain Name System)协议:用于将域名转换为 IP 地址,使得用户可以通过易记的域名访问网站,而无需记住复杂的 IP 地址。
网络的结构体系
网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。
每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的。每层向上层提供服务,同时使用下层提供的服务。
网络体系结构即指网络的层次结构和每层所使用的协议的集合。
OSI 七层模型
物理层:负责处理物理介质上的信号传输,包括电缆、光纤、无线等传输介质,以及信号的编码、解码、调制、解调等。相关协议有 EIA/TIA - 232、EIA/TIA - 449 等,规定了物理接口的电气、机械特性等。
数据链路层:将物理层接收到的信号转换为数据帧,实现相邻节点之间的数据传输。主要协议有以太网协议(IEEE 802.3),定义了以太网的帧格式、介质访问控制方法等;还有 PPP 协议,常用于拨号上网或点对点连接。
网络层:负责将数据从源节点传输到目的节点,通过路由选择算法确定数据传输的路径。主要协议是 IP 协议,规定了数据包的格式和寻址方式;还有 ICMP 协议,用于在网络设备之间传递控制消息,如错误报告、网络状态查询等。
传输层:为应用程序提供端到端的通信服务,确保数据的可靠传输或高效传输。主要协议有 TCP 协议,面向连接,提供可靠的数据传输服务,通过确认、重传等机制保证数据的准确性;UDP 协议则是无连接的,提供不可靠但高效的数据传输服务,适用于对实时性要求高的应用。
会话层:负责建立、维护和管理会话,如会话的建立、拆除、同步等。常见的协议有 NetBEUI 等,在局域网中应用较多。
表示层:对数据进行转换、加密、解密、压缩、解压缩等处理,确保不同系统之间能够正确地理解和处理数据。例如,JPEG 是图像压缩的标准,SSL/TLS 协议用于数据加密。
应用层:为用户提供各种网络应用服务,如 HTTP 协议用于网页浏览,SMTP 协议用于发送电子邮件,POP3 和 IMAP 协议用于接收电子邮件,DNS 协议用于域名解析等。
TCP/IP协议族体系
TCP/IP 模型是实际应用中更为广泛的网络结构体系,它将网络分为四层,与 OSI 七层模型有一定的对应关系。
网络接口和物理层:对应 OSI 模型的物理层和数据链路层,负责处理与物理网络的连接,包括各种网络设备的驱动程序以及物理介质上的数据传输。常见的协议和技术如以太网、Wi - Fi 等都属于这一层。
MAC 地址
定义:
MAC 地址(Media Access Control Address),也称为物理地址或硬件地址,是用于标识网络设备(如网卡)的唯一标识符。它是一个 48 位的二进制数,通常以十六进制表示,如00 - 16 - 3E - 00 - 1A - 2B。
特点:
唯一性:一般来说,每个网络设备的 MAC 地址在全球范围内是唯一的。这是由设备制造商在生产过程中烧录到网卡芯片中的,确保了不同设备之间的区分。
固化性:MAC 地址通常是固化在硬件中的,不会轻易改变。即使设备更换了网络环境,其 MAC 地址也保持不变。
作用:
在局域网中,MAC 地址用于在数据链路层实现设备之间的直接通信。当一个设备要向另一个设备发送数据时,它会在数据帧中包含目标设备的 MAC 地址,这样数据帧就可以在局域网内准确地传输到目标设备。
网络层:与 OSI 模型的网络层功能类似,主要协议是 IP 协议,负责数据包的路由和寻址。此外,还有 ICMP、IGMP 等协议,分别用于网络控制和组播管理。
IP 地址
定义:
IP 地址(Internet Protocol Address)是为互联网上的每一个网络和每一台主机分配的逻辑地址。目前广泛使用的是 IPv4 地址,它是一个 32 位的二进制数,通常以点分十进制表示,如192.168.1.1。随着互联网的发展,IPv6 地址逐渐得到应用,它是 128 位的二进制数,以冒号十六进制表示,如2001:0db8:85a3:0000:0000:8a2e:0370:7334。
特点:
层次性:IP 地址具有层次结构,分为网络部分和主机部分。网络部分用于标识设备所在的网络,主机部分用于标识该网络中的具体设备。通过这种层次结构,路由器可以根据网络部分快速地将数据包路由到目标网络。
可变性:IP 地址可以根据网络配置和需求进行动态分配或手动设置。在不同的网络环境中,设备可能会获得不同的 IP 地址。
作用:
IP 地址主要用于在网络层实现全球范围内的设备通信。它使得数据包能够在不同的网络之间进行路由和转发,最终到达目标设备。无论是在局域网还是广域网中,IP 地址都是实现数据传输和通信的关键标识。
传输层:同样包含 TCP 和 UDP 协议,功能与 OSI 模型的传输层相同。TCP 提供可靠的面向连接服务,UDP 提供不可靠的无连接服务。
应用层:涵盖了各种应用层协议,如 HTTP、SMTP、POP3、DNS、FTP(文件传输协议)、Telnet/ssh(远程登录)、NTP(网络时钟协议)、SNMP(简单网络管理协议)、RTP/RSTP(用于传输音视频的协议)等,为用户提供具体的网络应用服务。
网络的封包和拆包
封包是指在网络通信中,将上层应用的数据按照一定的格式和协议,封装成适合在网络中传输的数据包的过程
拆包是封包的逆过程,是指接收方在收到网络数据包后,按照相应的协议,将数据包中的数据和控制信息进行解析和提取,还原出原始的应用层数据的过程
TCP/IP协议下的数据包
网络相关知识
socket
Socket 是网络编程中用于实现进程间通信的核心资源与编程接口,也称为套接字。在操作系统层面,它被抽象为特殊的文件描述符,因此能够复用传统文件 I/O 操作函数(如read、write、close等)进行数据读写与资源管理。Socket 不仅支持 TCP/IP 协议族,还适用于 UDP、ICMP 等多种网络协议,其中面向连接的 TCP 协议通过三次握手建立可靠通信链路,能确保数据按序、无丢失传输,适用于 HTTP 请求、文件传输等场景;无连接的 UDP 协议则以数据报形式直接发送数据,无需建立连接,虽然不保证可靠性,但具有低延迟、高效率的特点,常用于 DNS 解析、实时视频流传输等对实时性要求较高的场景。
分类
1.流式套接字(SOCK_STREAM) 唯一对应TCP
提供了一个面向连接,可靠的数据传输服务,数据无差错,无重复的发送顺序接收。内射击流量控
制,避免数据流淹没慢的接收方。数据被看作式字节流,无长度限制。
2.数据包套接字(SOCK_DGRAM) 唯一对应UDP
提供无连接服务器,数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重
复,顺序发送,可能乱序接收。
3.原始套接字(SOCK_RAW) 对应多个协议,发送穿透了传输层,可以对较低层次协议如IP,ICMP直接访问
IP地址和端口号
IP 地址是互联网协议地址,用于在网络中唯一标识设备,如同现实中的家庭住址。它分为 IPv4 和 IPv6 两种格式,IPv4 由 32 位二进制数组成,通常表示为点分十进制形式,如 192.168.1.1;IPv6 则由 128 位二进制数组成,以冒号分隔的十六进制表示。
端口号是应用程序在设备上的标识,用于区分同一设备上不同的网络应用进程,类似于同一地址下不同的房间号。它是一个 16 位的整数,取值范围从 0 到 65535,其中 0 到 1023 为知名端口,被特定的系统服务所使用,如 80 端口用于 HTTP 服务,21 端口用于 FTP 服务等。保留端口1024-5000(不建议使用)可以使用的端口: 5000~65535,TCP端口号于UDP端口号独立。
网络里的通信是由 IP地址+端口号来决定的,它们的作用是共同实现网络中设备间的准确通信。IP 地址确保数据能到达目标设备,而端口号则保证数据能被正确的应用程序接收和处理。
字节序
介绍
字节序是指数据在计算机内存中存储或传输时字节的顺序。主要有大端字节序(Big - Endian)和小端字节序(Little - Endian)两种。在不同字节序的系统间进行数据传输或共享时,需进行字节序转换,否则会导致数据错误。例如,大端序系统发送的数据,小端序系统接收后需转换字节序才能正确解析。
大端字节序:也叫大端序或大字节序。数据的高位字节存于低地址,低位字节存于高地址。比如对于 32 位整数0x12345678,高位字节0x12存于内存低地址,接着依次是0x34、0x56、0x78存于更高地址,就像按从左到右(高位在前)的顺序存储,符合人类正常思维习惯,常用于网络协议、一些文件格式等。
小端字节序:也称小端序或小字节序。与大端字节序相反,数据的低位字节存于低地址,高位字节存于高地址。对于0x12345678,在小端序中0x78存于内存低地址,接着是0x56、0x34、0x12存于更高地址,如同从右到左(低位在前)存储,很多微处理器和微控制器默认采用小端字节序,因为其按字节读取内存时更高效。
如果CPU访问的是字符串,则不存在大小端问题,一般来说X86/ARM 采用小端模式,power/miop:arm作为路由时采用大端模式,网络传输的时候采用大端模式
字节序转换函数
把给定系统所采用的字节序称为主机字节序,为了避免不同类别主机之间在数据交换时由于对于字
节序的不同而导致的差错,引入了网络字节序。
主机字节序到网络字节序
u_long htonl(u_long hostlong);
u_short htons(u_short short);
网络字节序到主机字节序
u_long ntohl(u_long hostlong);
u_short ntohs(u_short short);
IP地址的转换函数
1. inet_aton() / inet_ntoa()(IPv4 专用)
//将点分十进制字符串转换为 32 位二进制整数(网络字节序)
int inet_aton(const char *cp, struct in_addr *inp);
//返回值:成功返回 1,失败返回 0。
/*例如:
struct in_addr addr;
inet_aton("192.168.1.1", &addr);
// addr.s_addr 存储二进制IP地址*/
//将 32 位二进制整数(网络字节序)转换为点分十进制字符串
char *inet_ntoa(struct in_addr in);
/*例如:
struct in_addr addr;
addr.s_addr = htonl(0xC0A80101); // 192.168.1.1的二进制表示
printf("IP: %s\n", inet_ntoa(addr)); // 输出: 192.168.1.1 */
2. inet_addr()
//将点分十进制字符串直接转换为 32 位二进制整数(网络字节序)
//局限性:不能用于255.255.255.255的转换
in_addr_t inet_addr(const char *cp);
/*例如:
struct sockaddr_in addr;
addr.s_addr = inet_addr("192.168.1.1"); */
3. inet_pton() / inet_ntop()(IPv4/IPv6 通用)
//将点分十进制字符串(IPv4)或冒号十六进制字符串(IPv6)转换为二进制格式
int inet_pton(int af, const char *src, void *dst);
/*参数:
af:地址族(AF_INET 或 AF_INET6)。
src:输入字符串。
dst:输出二进制地址(如struct in_addr或struct in6_addr)*/
/*例如:
struct in_addr addr;
inet_pton(AF_INET, "192.168.1.1", &addr);*/
//将二进制 IP 地址转换为文本格式
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
//size:目标缓冲区大小
/*例如:
struct in_addr addr;
char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, str, INET_ADDRSTRLEN);*/
TCP编程
三次握手和四次挥手
三次握手
TCP 三次握手是 TCP 协议用于在客户端和服务器之间建立可靠连接的过程 ,通过三个步骤完成:
第一次握手:客户端发送一个 SYN(同步序列编号)报文段给服务器,SYN 标志位被置为 1,表明这是连接请求。同时,客户端随机生成一个初始序列号 seq = x(x 为某个值),放在 TCP 头部序列号字段。发送后,客户端进入 SYN_SENT 状态,等待服务器确认。
第二次握手:服务器收到客户端 SYN 报文段,确认 SYN 标志位为 1 后,向客户端发送一个 SYN + ACK(同步 / 确认应答)报文段。该报文段中 SYN 标志位为 1,表示服务器同意建立连接;ACK 标志位为 1,确认号 ack = x + 1(即客户端初始序列号加 1)。此外,服务器随机生成自己的初始序列号 seq = y(y 为某个值),放在 TCP 头部序列号字段。发送后,服务器进入 SYN_RCVD(同步接收)状态,等待客户端确认。
第三次握手:客户端收到服务器的 SYN + ACK 报文段,检查 ACK 标志位和确认号 ack 的值(是否为 x + 1 )。若正确,客户端再发送一个 ACK 报文段给服务器以确认连接建立。此 ACK 报文段中,ACK 标志位为 1,确认号 ack = y + 1(即服务器初始序列号加 1),序列号 seq 通常是客户端上一个 SYN 报文段中的序列号 x + 1 。客户端发送完 ACK 报文段后进入 ESTABLISHED(已建立连接)状态;服务器收到客户端的 ACK 报文段后,也进入 ESTABLISHED 状态,此时 TCP 连接建立完成,双方可开始传输数据。
四次挥手
TCP 四次挥手是用于断开 TCP 连接的过程 ,因为 TCP 连接是全双工的,每个方向需单独关闭,具体步骤如下:
第一次挥手:若客户端决定关闭连接,它发送一个带有 FIN 标志的报文段给服务器,进入 FIN - WAIT - 1 状态。此时,客户端不再发送数据,但仍可接收服务器发来的数据。
第二次挥手:服务器收到客户端的 FIN 报文段后,发送一个带有 ACK 标志的报文段作为响应,进入 CLOSE - WAIT 状态。此时服务器停止向客户端发送数据,但还能接收客户端的数据。服务器发送的 ACK 报文段中,确认号为收到的客户端序列号加 1 。
第三次挥手:服务器在发送完所有待发数据后,发送一个带有 FIN 标志的报文段给客户端,进入 LAST_ACK 状态 ,表示服务器也准备好关闭连接了。
第四次挥手:客户端收到服务器的 FIN 后,发送一个带有 ACK 标志的报文段作为确认,进入 TIME - WAIT 状态。客户端发送的 ACK 报文段中,确认号为收到的服务器序列号加 1 。服务器收到 ACK 后,确认无误就进入 CLOSED 状态,不再向客户端发送数据。客户端在 TIME - WAIT 状态等待 2 倍的 MSL(Maximum Segment Lifetime,报文段最长寿命 )时间后,也进入 CLOSED 状态,至此 TCP 连接完全关闭,双方释放相关资源。等待 2MSL 时间主要是为确保网络中可能残留的与该连接有关的旧报文段都已消失,避免干扰后续连接,同时也保证对方能收到最后一个 ACK 报文段,可靠地关闭连接。
TCP编程API
1.socket函数
创建套接字的系统调用函数
int socket(int domain,int type,int protocol);
/*参数说明:
*domain:指定协议族,决定套接字地址类型 :
AF_INET:用于 IPv4 网络协议
AF_INET6:用于 IPv6 网络协议
AF_UNIX:用于 Unix 域协议
AF_UNSPEC:不指定协议族
AF_NETLINK:用于用户空间和内核空间之间的通信
AF_PACKET:用于直接访问网络层
*type:指定套接字类型,决定数据传输格式和方式:
SOCK_STREAM:通常使用 TCP 协议
SOCK_DGRAM:通常使用 UDP 协议
SOCK_RAW:提供原始网络协议访问
*protocol:指定具体协议类型。多数情况设为 0,表示默认协议,系统会根据domain和type选择合适协议
返回值
成功时,返回一个非负整数,即套接字描述符(socket descriptor)
失败时,返回 - 1*/
2.bind函数
将一个套接字与特定的本地地址(包含 IP 地址和端口号 )绑定起来
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*参数说明:
*sockfd:已创建的套接字描述符,通过调用socket()函数获得 ,用于标识要绑定地址的套接字。
*addr:指向sockaddr结构体的指针,该结构体存放了要绑定的本地地址信息,包括地址族、IP 地址和端口号等 。实际使用中常根据地址族类型,使用sockaddr_in(IPv4 )或sockaddr_in6(IPv6 )等结构体,再强制转换为sockaddr*类型传入。
*addrlen:表示addr结构体的长度,一般用sizeof(struct sockaddr_in) (针对 IPv4 )或sizeof(struct sockaddr_in6)(针对 IPv6 )获取 。
返回值
成功时返回 0
失败时返回 - 1 */
相关结构体
//通用结构体
struct sockaddr{
sa_family_t sa_family;
char sa_data[4];
}
//IPv4 通信结构体
struct sockaddr_in {
sa_family_t sin_family; // 地址族,必须为 AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IPv4 地址结构体
char sin_zero[8]; // 填充字节,必须全部置零
};
struct in_addr {
uint32_t s_addr; // IPv4 地址(网络字节序)
};
//IPv6 通信结构体
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族,必须为 AF_INET6
in_port_t sin6_port; // 端口号(网络字节序)
uint32_t sin6_flowinfo; // IPv6 流信息
struct in6_addr sin6_addr; // IPv6 地址结构体
uint32_t sin6_scope_id; // 范围 ID(用于链路本地地址)
};
struct in6_addr {
unsigned char s6_addr[16]; // IPv6 地址(128 位)
};
3.listen函数
listen()
函数用于将套接字转换为被动监听状态,使其能够接受来自客户端的连接请求
int listen(int sockfd, int backlog);
/*参数:
*sockfd:由 socket() 创建并经 bind() 绑定地址的套接字描述符。
*backlog:一般填5,指定允许的未处理连接请求队列的最大长度。当队列满时,新的连接请求会被拒绝
返回值:
成功:返回 0
失败:返回 -1 */
4.accept()函数
accept() 函数用于从已完成连接队列中取出一个客户端连接,并返回一个新的套接字描述符用于与该客户端通信。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*参数:
*sockfd:由 socket() 创建、bind() 绑定地址并经 listen() 转换为监听状态的套接字描述符(称为 监听套接字)。
*addr:存储客户端的地址信息,指向sockaddr结构体的指针,实际使用中常根据地址族类型,使用sockaddr_in(IPv4 )或sockaddr_in6(IPv6 )等结构体,再强制转换为sockaddr*类型传入。
*addrlen:指向 socklen_t 类型的指针,表示 addr 结构体的长度。调用前需初始化为 addr 的大小,返回时存储实际地址长度。
返回值:
成功:返回一个新的套接字描述符(称为 连接套接字),用于与客户端通信。
失败:返回 -1*/
5.客户端连接函数connect()
connect()函数用于在客户端建立与服务器的连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*参数:
*sockfd:通过socket()函数创建的套接字描述符。
*addr:指向目标服务器的地址结构体的指针,通常为struct sockaddr类型。对于 IPv4,一般使用struct sockaddr_in结构体并进行强制转换,该结构体包含服务器的地址族、端口号和 IP 地址等信息。
*addrlen:addr结构体的长度。
返回值:
成功时返回 0
失败时返回 -1*/
6.send()函数
send() 函数用于通过已连接的套接字发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
/*参数:
*sockfd:已连接的套接字描述符(如 connect() 或 accept() 返回的套接字)。
*buf:指向要发送数据的缓冲区指针。
*len:要发送的字节数(size_t 类型)。
*flags:可选标志,常用值:
0:默认行为。
MSG_DONTWAIT:非阻塞模式(等效于 O_NONBLOCK)。
MSG_NOSIGNAL:避免在连接断开时发送 SIGPIPE 信号。
返回值:
成功:返回实际发送的字节数(可能小于 len)。
失败:返回 -1*/
7.recv()函数
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
/*参数:
*sockfd:已连接的套接字描述符(如 connect() 或 accept() 返回的套接字)。
*buf:指向接收缓冲区的指针。
*len:缓冲区的最大长度(字节)。
*flags:控制接收行为的标志位,常用值:
0:默认行为,与 read() 相同。
MSG_DONTWAIT:非阻塞模式,立即返回(等效于 O_NONBLOCK)。
MSG_OOB:接收带外数据(TCP 紧急数据)。
MSG_PEEK:预览数据(数据留在缓冲区,下次读取仍可用)。
返回值:
> 0:成功接收的字节数。
0:表示连接已正常关闭(TCP FIN 包)
-1:出错,*/
并发服务器
1.TCP多线程服务器
举例(服务器):
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <strings.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#define QUIT_STR "QUIT"
#define BUFSIZE 1024
#define BACKLOG 5
#define SERV_PORT 8888
#define SERV_IP_ADDR "192.168.1.14"
void child_data_handle(int signum)
{
if(SIGCHLD == signum)
{
waitpid(-1,NULL,WNOHANG);
}
}
void* client_data_handle(void *arg);
int main()
{
int fd = -1;
signal(SIGCHLD,child_data_handle);
struct sockaddr_in sin;
//1.创建套接字
fd = socket(AF_INET,SOCK_STREAM,0);
if(fd < 0)
{
perror("socket");
return -1;
}
bzero(&sin,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);
//sin.sin_addr.s_addr = inet_addr(SERV_IP_ADDR);
sin.sin_addr.s_addr = INADDR_ANY;//自动获取本地IP地址
/*if(iney_pton(AF_INIT,SERV_IP_ADDR,(void *)&sin.sin_addr.s_addr) != 0)
{
perror("inet_pton");
exit(1);
}*/
//2.绑定
if( bind(fd,(struct sockaddr *)&sin,sizeof(sin)) < 0)
{
perror("bind");
exit(1);
}
//3.监听
if(listen(fd,BACKLOG) < 0)
{
perror("listen");
exit(1);
}
//4.连接
pthread_t tid;
int newfd = -1;
struct sockaddr_in cin;
socklen_t addrlen = sizeof(cin);
while(1)
{
newfd = accept(fd,(struct sockaddr *)&cin,&addrlen);
if(newfd < 0)
{
perror("accept");
exit(1);
}
char ipv4_addr[16];
if(!inet_ntop(AF_INET,(void*)&cin.sin_addr.s_addr,ipv4_addr,sizeof(sin)))
{
perror("inet_ntop");
exit(1);
}
printf("client:(%s,%d)is connect!\n",ipv4_addr,ntohs(cin.sin_port));
//连接成功就创建新线程处理
pthread_create(&tid,NULL,client_data_handle,(void *)&newfd);
}
}
close(fd);
}
//线程函数
void * client_data_handle(void * arg)
{
int ret = -1;
char buf[BUFSIZE];
int newfd = *(int *)arg;
printf("pthread:newfd = %d\n",newfd);
while(1)
{
do{
bzero(buf,BUFSIZE);
ret = read(newfd,buf,BUFSIZE-1);
if(ret < 0 )
{
printf("read error!\n");
break;
}
}while(ret < 1);
if(!ret)
{
printf("no data!\n");
break;
}
printf("pthread %d data is:%s\n",newfd,buf);
if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
{
printf("client is exiting!\n");
break;
}
}
close(newfd);
return NULL;
}
2.TCP多进程服务器
和上面代码不同的是accept,每有一个客户端连接就创建一个子进程调用函数取处理信息交互
//4.accept
pid_t pid;
int newfd = -1;
struct sockaddr_in cin;
socklen_t addrlen = sizeof(cin);
while(1)
{
newfd = accept(fd,(struct sockaddr *)&cin,&addrlen);
if(newfd < 0)
{
perror("accept");
break;
}
pid = fork();
if(pid < 0)
{
perror("fork");
break;
}
if(pid == 0)
{
char ipv4_addr[16];
if(!inet_ntop(AF_INET(void*)&cin.sin_addr.s_addr,ipv4_addr,sizeof(sin)))
{
perror("inet_ntop");
exit(1);
}
printf("client:(%s,%d)is connect!\n",ipv4_addr,ntohs(cin.sin_port));
client_data_handle((void *)&newfd);
close(fd);
}
if(pid > 0)
{
close(newfd);
}
}
UDP编程
相关函数
1.receivefrom()
recvfrom() 是网络编程中用于接收数据的核心函数,尤其适用于无连接的 UDP 协议
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
/*参数说明:
*sockfd:已创建并绑定地址的套接字描述符(通过 socket() 和 bind() 获取)。
*buf:指向接收数据缓冲区的指针,用于存储接收到的数据。
*len:缓冲区的最大长度(字节),防止数据溢出。
*flags:控制接收行为的标志位,常用值:
0:默认行为,阻塞等待数据。
MSG_DONTWAIT:非阻塞模式,若无数据立即返回 -1(errno 为 EWOULDBLOCK)。
MSG_PEEK:预览数据(数据保留在接收队列,下次仍可读取)。
*src_addr:存储发送方的地址信息结构体的指针,通常为struct sockaddr类型。对于 IPv4,一般使用struct sockaddr_in结构体并进行强制转换,该结构体包含服务器的地址族、端口号和 IP 地址等信息。
*addrlen:指向 socklen_t 类型的指针,src_addr 结构体的初始大小,返回时存储实际地址长度。
返回值:
成功:返回实际接收的字节数(可能小于 len)。
连接关闭:TCP 中返回 0(UDP 无此情况,因无连接状态)。
失败:返回 -1*/
2.sendto()
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
/*参数:
*sockfd:已创建的套接字描述符(通过 socket() 获取)。
*buf:指向要发送数据的缓冲区指针。
*len:要发送的字节数(size_t 类型)。
*flags:控制发送行为的标志位,常用值:
0:默认行为,阻塞发送。
MSG_DONTWAIT:非阻塞模式,若发送缓冲区已满立即返回 -1(errno 为 EWOULDBLOCK)。
MSG_OOB:发送带外数据(仅用于 TCP,UDP 不支持)。
*dest_addr:指向目标地址的结构体指针(如 struct sockaddr_in 或 struct sockaddr_in6),指定数据的接收方。实际使用中常根据地址族类型,使用sockaddr_in(IPv4 )或sockaddr_in6(IPv6 )等结构体,再强制转换为sockaddr*类型传入。
*addrlen:dest_addr 结构体的长度(如 sizeof(struct sockaddr_in))。
返回值:
成功:返回实际发送的字节数(通常等于 len)。
失败:返回 -1*/
IO多路复用
在unix/linu下主要有四种I/O模式:
模式 | 阻塞特性 | 核心机制 | 典型应用场景 | 优点 | 缺点 |
---|---|---|---|---|---|
阻塞 I/O | 全程阻塞 | 等待数据就绪 | 单线程简单应用 | 实现简单 | 无法并发,资源利用率低 |
非阻塞 I/O | 立即返回,需轮询 | 主动查询 I/O 状态 | 需快速响应的场景 | 无长时间阻塞 | 轮询消耗 CPU |
I/O 多路复用 | 阻塞在 select/poll/epoll | 集中监视多个 fd | 高并发连接的服务器 | 单线程处理多连接,效率高 | 编程模型较复杂 |
异步 I/O | 全程非阻塞 | 内核完成后通知 | 高吞吐量的 I/O 密集型应用 | 真正的非阻塞,资源利用率最高 | 实现复杂,依赖系统支持 |
IO 多路复用(I/O Multiplexing)是一种高效的 I/O 处理机制,允许单个进程同时监视多个文件描述符(如套接字、文件、管道等)的读写状态,避免阻塞在单个 I/O 操作上,从而显著提高程序的并发处理能力。
1.非负整数
2.从最小可用的数字来分配
3.每个进程启动时默认打开0、1、2三个文件描述符
先构造一张又关描述符的表,然后调用一个函数,当这些文件描述符中的一个或多个已准备好进行
IO时函数才返回。
函数返回时告诉进程那个描述符已就绪,可以进行IO操作
相关函数
1.fd_set
fd_set readfds; // 存储待监听的文件描述符集合
FD_ZERO(&readfds); // 清空集合
FD_SET(sockfd, &readfds); // 添加套接字到集合
FD_CLR(sockfd, &readfds); // 从集合移除套接字
FD_ISSET(sockfd, &readfds); // 判断套接字是否在集合中(就绪时返回真)
2.select
int select(
int maxfdp1, // 监听的最大文件描述符值 +1
fd_set *readfds, // 读事件集合(传入&传出)
fd_set *writefds, // 写事件集合(通常填NULL)
fd_set *exceptfds, // 异常事件集合(通常填NULL)
struct timeval *timeout // 超时设置
);
/*返回值:
>0:就绪的文件描述符总数
0:超时
-1:出错*/
//超时设置
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
/*常用模式:
永久阻塞:timeout = NULL
非阻塞:timeout.tv_sec = 0; timeout.tv_usec = 0
定时阻塞:timeout.tv_sec = 5;(5 秒超时)*/
3.select() 事件处理中 FD_ISSET 判断
//select退出后:集合表示有数据的文件描述符集合
if(FD_ISSET(fd,set))
{
//1.如果监听套接字有数据,新的客户端进行连接,则accept
//2.若建立连接的套接字有数据,则去读read
}
使用步骤举例
while(1)
{
FD_ZERO(&rset);
FD_SET(0,&rset);
FD_SET(fd,&rset);
maxfd = fd;
tout.tv_sec = 5;
tout.tv_usec = 0;
select(maxfd+1,&rset,NULL,NULL,&tout);
if(FD_ISSET(0,&rset))//stdin have data
{
bzero(buf,BUFSIZE);
do
{
ret = read(0,buf,BUFSIZE-1);
if(ret == -1)
{
perror("read");
continue;
}
}while(ret<0);
if(!ret) continue;
if(write(fd,buf,strlen(buf))<0)
{
perror("write");
continue;
}
}
if(FD_ISSET(fd,&rset))//server have data
{
bzero(buf,BUFSIZE);
do
{
ret = read(fd,buf,BUFSIZE-1);
if(ret == -1)
{
perror("read");
continue;
}
if(!ret) break;
printf("Sever data is:%s\n",buf);
}while(ret < 0);
}