目录
1、UDP
UDP(User Datagram Protocol)是一种不可靠的传输层协议,用于在IP网络中传输数据包。
1.1、UDP的特点
- 不可靠:UDP不保证数据包的到达和顺序,数据可能会丢失、 corruption 或重复。
- 无连接:UDP不需要建立连接,而是每个数据包单独地传输。
- 无序序:UDP不保证数据包的顺序,可能会出现乱序。
- 无确认机制:UDP不需要确认数据包的到达。
1.2、UDP的使用场景
- 实时应用:UDP通常用于实时应用,如视频流、音频流、游戏等,这些应用需要快速传输数据,而不关心数据的可靠性。
- 小数据包传输:UDP适用于小数据包的传输,如DNS查询、DHCP分配等,这些应用不需要传输大量数据。
- 网关设备:UDP通常用于网关设备的数据传输,如路由器、交换机等,这些设备需要快速传输数据,而不关心数据的可靠性。
1.3、UDP的优缺
优点:
- 高速传输:UDP的传输速度快,能够快速传输数据。
*低延迟:UDP的传输延迟低,能够实时传输数据。
缺点:
- 不可靠:UDP的数据传输不可靠,可能会丢失、 corruption 或重复。
- 无确认机制:UDP没有确认机制,无法确定数据是否到达目的地。
1.4、UDP的实现
UDP的实现主要涉及到以下几个方面:
- UDP头:UDP头包含源端口号、目的端口号、数据长度和检验和。
- 数据传输:UDP将数据分割成小包,添加头信息,然后传输到目的地。
- 接收端处理:接收端将收到的数据包组装成原始数据,并检查是否完整和正确。
总的来说,UDP是一种不可靠的传输协议,适用于实时应用和小数据包传输。但是,它的不可靠性和缺少确认机制也使得它在某些情况下不可用。
1.5示例
服务器端
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有接口
server_addr.sin_port = htons(PORT);
// 绑定套接字到地址
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
printf("UDP server started on port %d", PORT);
while (1) {
// 接收数据
ssize_t n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, &client_len);
if (n < 0) {
perror("recvfrom failed");
continue;
}
buffer[n] = '\0'; // 添加字符串结束符
printf("Received from %s:%d: %s", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);
// 发送回显
sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&client_addr, client_len);
}
close(sockfd);
return 0;
}
客户端
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1" // 服务器IP地址
#define SERVER_PORT 8888
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(SERVER_PORT);
printf("Enter message: ");
fgets(buffer, BUFFER_SIZE, stdin);
buffer[strcspn(buffer, "")] = 0; // 去除换行符
// 发送数据
if (sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("sendto failed");
exit(EXIT_FAILURE);
}
// 接收数据
ssize_t n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, NULL, NULL);
if (n < 0) {
perror("recvfrom failed");
exit(EXIT_FAILURE);
}
buffer[n] = '\0';
printf("Received from server: %s", buffer);
close(sockfd);
return 0;
}
2.IO模型
IO模型是操作系统中一种重要的概念,用于描述进程或线程与IO设备之间的交互方式。常见的IO模型有以下四种:
-
阻塞I/O(Blocking I/O)
在阻塞I/O模型中,进程或线程在等待IO操作完成时将被阻塞,直到IO操作完成后才继续执行。
-
非阻塞I/O(Non-Blocking I/O)
在非阻塞I/O模型中,进程或线程在等待IO操作完成时不会被阻塞,可以继续执行其他任务。
-
I/O多路复用(I/O Multiplexing)
在I/O多路复用模型中,进程或线程可以同时监控多个IO设备的状态,而不需要阻塞或轮询每个设备。
-
异步I/O(Asynchronous I/O)
在异步I/O模型中,IO操作将在后台执行,而进程或线程可以继续执行其他任务,不需要等待IO操作完成。
2.1、I/O多路复用
I/O多路复用(I/O Multiplexing)是一种技术,允许单个进程监控多个文件描述符(File Descriptor),并在其中的一个或多个文件描述符上进行I/O操作,而不需要创建多个进程或线程。该技术可以提高系统的并发性和效率。
I/O多路复用有以下几个主要技术:
- select()函数:该函数监控多个文件描述符,并返回哪些文件描述符已经就绪可以进行I/O操作。
- poll()函数:该函数监控多个文件描述符,并返回哪些文件描述符已经就绪可以进行I/O操作。
- epoll()函数(Linux特有):该函数监控多个文件描述符,并返回哪些文件描述符已经就绪可以进行I/O操作。
在使用I/O多路复用时,需要完成以下步骤:
- 创建文件描述符数组,用于存储需要监控的文件描述符。
- 使用select()、poll()或epoll()函数监控文件描述符数组,并返回哪些文件描述符已经就绪可以进行I/O操作。
- 对于就绪的文件描述符,进行I/O操作。
- 重复步骤2和3,直到所有文件描述符都已经处理完毕。
2.2、多路复用select()的实现
cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
int max_fd = 0;
fd_set read_fds;
char buffer[256];
// 创建服务器套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
exit(1);
}
// 设置服务器套接字地址和端口
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
// 绑定服务器套接字
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
exit(1);
}
// 监听客户端连接
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("listen");
exit(1);
}
while (1) {
// 创建文件描述符集
FD_ZERO(&read_fds);
FD_SET(server_fd, &read_fds);
max_fd = server_fd;
// 等待客户端连接
if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) < 0) {
perror("select");
exit(1);
}
// 处理客户端连接
if (FD_ISSET(server_fd, &read_fds)) {
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
exit(1);
}
printf("Connected by client IP address %s and port %d",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 将客户端文件描述符添加到文件描述符集中
FD_SET(client_fd, &read_fds);
if (client_fd > max_fd) {
max_fd = client_fd;
}
}
// 处理客户端数据
for (int i = server_fd; i <= max_fd; i++) {
if (FD_ISSET(i, &read_fds)) {
if (i == server_fd) {
// 处理客户端连接
} else {
// 读取客户端数据
read(i, buffer, 256);
printf("Received from client %s:%d: %s",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);
}
}
}
}
return 0;
}
2.3、多路复用poll()的实现
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <poll.h>
#define PORT 8080
#define MAX_CLIENTS 10
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fds[MAX_CLIENTS];
int client_fds_count = 0;
struct pollfd poll_fds[MAX_CLIENTS];
// 创建服务器 socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket");
return -1;
}
// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 绑定服务器 socket
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
return -1;
}
// 监听客户端连接
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("listen");
return -1;
}
while (1) {
// 创建 poll 结构体
for (int i = 0; i < MAX_CLIENTS; i++) {
poll_fds[i].fd = -1;
poll_fds[i].events = POLLIN;
}
// 读取可读的客户端 socket
for (int i = 0; i < client_fds_count; i++) {
poll_fds[i].fd = client_fds[i];
}
// 进行 poll
int ret = poll(poll_fds, client_fds_count, -1);
if (ret < 0) {
perror("poll");
return -1;
}
// 处理 poll 事件
for (int i = 0; i < client_fds_count; i++) {
if (poll_fds[i].revents & POLLIN) {
// 可读客户端 socket
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
return -1;
}
// 添加客户端 socket 到数组中
client_fds[client_fds_count++] = client_fd;
}
}
}
return 0;
}
2.4、select()与poll()的区别
poll
和select
都是I/O多路复用的系统调用,它们的主要区别在于:
-
文件描述符的表示方式:
select
使用三个位图(readfds
、writefds
、exceptfds
)来表示关注的文件描述符集合,每个位图的大小固定,最大只能处理FD_SETSIZE个文件描述符(通常是1024)。而poll
使用一个pollfd
结构体的数组来表示关注的文件描述符集合,每个pollfd
结构体包含一个文件描述符和事件掩码,理论上可以处理的文件描述符数量不受限制,只受限于系统内存。 因此,poll
在处理大量文件描述符时具有更高的效率和可扩展性。 -
事件通知方式:
select
返回后,需要遍历三个位图来确定哪些文件描述符就绪,效率较低。poll
返回后,可以直接从pollfd
数组中获取就绪的文件描述符及其事件,效率更高。 -
出错处理:
select
和poll
在出错时都会返回-1,并设置errno
。 但是poll
的出错处理相对更清晰,因为pollfd
数组中的每个元素都能独立地反映其状态,方便定位错误。
总而言之,poll
相较于select
,在处理文件描述符数量和效率上都有改进,但两者本质上都是基于轮询的机制,都需要内核遍历文件描述符集合,因此在处理大量文件描述符时,性能仍然会成为瓶颈。 epoll
作为更高效的I/O多路复用机制,在实际应用中更受欢迎。
3、套接字属性
-
地址族(Address Family): 指定套接字使用的网络协议族,例如AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(Unix域套接字)。 这决定了套接字能够连接到哪种类型的网络。
-
套接字类型(Socket Type): 定义套接字的通信方式,例如:
SOCK_STREAM
:面向连接的可靠传输,例如TCP。 数据有序可靠地到达,保证数据完整性。SOCK_DGRAM
:面向无连接的不可靠传输,例如UDP。 数据包独立传输,可能丢失、乱序或重复。SOCK_RAW
:原始套接字,允许访问网络协议栈的底层,通常用于网络编程的底层操作,例如网络监控和数据包分析。
-
协议(Protocol): 指定使用的网络协议,例如IPPROTO_TCP、IPPROTO_UDP。 通常由套接字类型隐式决定,但有些情况下可以明确指定。
-
缓冲区大小(Buffer Size): 发送和接收数据的缓冲区大小,可以影响性能。 过小可能导致频繁的I/O操作,过大可能浪费内存。
-
连接状态(Connection State): 描述套接字的连接状态,例如已连接、监听、已关闭等。 这对于管理连接至关重要。
-
选项(Options): 可以设置各种选项来控制套接字的行为,例如:
SO_REUSEADDR
:允许重用本地地址和端口。SO_LINGER
:控制关闭套接字时的行为。SO_SNDBUF
和SO_RCVBUF
:设置发送和接收缓冲区大小。SO_KEEPALIVE
:启用心跳机制,检测连接是否仍然有效。
这些属性共同定义了套接字的特性和行为,决定了它如何参与网络通信。 通过设置不同的属性,可以创建不同类型的套接字以满足不同的网络编程需求。
3.1、套接字属性查看与修改
int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
cpp
参数:
sockfd:套接字
level:设置属性层
SOL_SOCKET:通用套接字层
IPPROTO_IP:IP层
IPPRO_TCP:TCP层
optname:指定操作,一般用宏表示
optval:设置属性对应的值
optlen:设置属性对应值长度
返回值:
成功返回0,失败返回-1
3.1.1、SOL_SOCKET
cpp
optname optval类型
**SO_BROADCAST** 允许发送广播数据 int
SO_DEBUG 允许调试 int
SO_DONTROUTE 不查找路由 int
SO_ERROR 获得套接字错误 int
SO_KEEPALIVE 保持连接 int
SO_LINGER 延迟关闭连接 struct linger
SO_OOBINLINE 带外数据放入正常数据流 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
**SO_REUSEADDR** 允许重用本地地址和端口 int
SO_TYPE 获得套接字类型 int
SO_BSDCOMPAT 与BSD系统兼容 int
3.1.2、IPPROTO_IP
cpp
optname optval类型
IP_ADD_MEMBERSHIP 将指定IP加入到组播组中 struct ip_mreq
IP_MULTICAST_IF 允许开启组播报文的接口 struct ip_mreq
3.1.3、IPPRO_TCP
cpp
optname optval类型
TCP_MAXSEG TCP最大数据段的大小 int
TCP_NODELAY 不使用Nagle算法 int
3.1.4、常见的错误
bind failed: Address already in use
错误原因:上次连接的TCP还没有完全断开(涉及tcp的分手过程),端口被占用
解决方式:1、显示的是地址错误,其实修改端口号就可以了,但是多次的话比较麻烦,可以采用 第二种方式,永绝后患
2、int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen); 调用该函数,将SOL_SOCKET 这个参数设置为**SO_REUSEADDR** 允许重用本 地地址和端口。
4、tcp三次握手和四次挥手
三次握手和四次挥手是TCP连接建立和关闭过程中使用的机制,保证了可靠的数据传输。
三次握手 (Three-way handshake): 用于建立TCP连接。
-
SYN (同步): 客户端向服务器发送一个SYN包,其中包含客户端选择的初始序列号(ISN)。这个包表示客户端希望建立连接。
-
SYN-ACK (同步-确认): 服务器收到SYN包后,向客户端发送一个SYN-ACK包。这个包包含服务器选择的初始序列号(ISN)以及对客户端SYN包的确认号(ACK),确认号等于客户端的ISN加1。
-
ACK (确认): 客户端收到SYN-ACK包后,向服务器发送一个ACK包。这个包确认收到了服务器的SYN-ACK包,并包含服务器ISN加1的确认号。 至此,连接建立成功。
四次挥手 (Four-way handshake): 用于关闭TCP连接。
-
FIN (结束): 客户端向服务器发送一个FIN包,表示客户端不再发送数据,但仍可以接收数据。这个包包含客户端的序列号。
-
ACK (确认): 服务器收到FIN包后,向客户端发送一个ACK包,确认收到了客户端的FIN包。这个包包含客户端序列号加1的确认号。 注意,服务器此时可能仍然有数据要发送给客户端。
-
FIN (结束): 服务器发送完所有数据后,向客户端发送一个FIN包,表示服务器也不再发送数据。这个包包含服务器的序列号。
-
ACK (确认): 客户端收到服务器的FIN包后,向服务器发送一个ACK包,确认收到了服务器的FIN包。这个包包含服务器序列号加1的确认号。 至此,连接关闭。
三次握手和四次挥手区别的根本原因在于TCP连接是全双工的。 客户端和服务器都可以同时发送和接收数据。 关闭连接需要分别处理客户端和服务器的数据发送方向,因此需要四次挥手来确保双方都正确地关闭连接。 如果只有三次挥手,则无法保证服务器已经发送完所有数据。