在 UDP 协议中,有两个标志位:Checksum 和 Length。
Checksum 标志位用于检验 UDP 报文的传输过程中是否发生了数据损坏或丢失,以保证数据的完整性。校验和计算的范围是包括了整个 UDP 报文和 IP 报头(如果有)的所有数据。接收端收到 UDP 数据包后会重新计算校验和,如果发现与发送端计算的校验和不一致,则认为该数据包出错或被篡改,会丢弃该数据包或向发送端反馈错误信息。
Length 标志位则用于将 UDP 报文的长度信息传送给接收方,以便接收方正确解析报文。在 UDP 中,所有数据报都作为 IP 数据报的有效载荷传输,因此每个 UDP 数据包的长度均不一定相同。为了保证接收端能够正确识别 UDP 数据包,发送端需要将 UDP 报文的长度信息封装在 Length 标志位中,然后传输给接收端。
相对于 TCP 协议,UDP 协议使用的标志位较少,因为其特性是无连接的、不可靠的传输。另外需要注意的是,UDP 协议中没有确认、重传机制,因此在数据传输过程中,不能保证数据的可靠性和顺序性,需要应用程序自己来进行处理。
- 使用 `fcntl` 函数:通过调用 `fcntl` 函数来设置非阻塞模式。使用 `F_GETFL` 获取文件描述符的标志位,然后使用 `F_SETFL` 设置 `O_NONBLOCK` 标志位来将其设置为非阻塞模式。
3. 这个送分题:
bind
【回答 使用的epoll 来实现I/O多路复用】
num_ready_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
-1 是一直 循环监听
`epoll_wait` 函数是用于等待就绪的事件并返回的 epoll 函数族中的一个函数。它的原型如下:
```c
#includeint epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
```参数解释:
- `epfd`:epoll 文件描述符,通过 `epoll_create` 或 `epoll_create1` 函数创建的 epoll 实例的文件描述符。
- `events`:用于存储就绪事件的数组。`events` 是一个指向 `struct epoll_event` 数组的指针。
- `maxevents`:`events` 数组的容量,表示最多能够接收多少个事件。
- `timeout`:等待的超时时间,单位为毫秒。当 `timeout` 设置为 -1 时表示永久等待,直到有事件就绪;当设置为 0 时表示立即返回,非阻塞地查询事件;当设置为正数时,表示等待指定时间内的就绪事件。返回值:
- 成功时,返回就绪事件的数量。返回值大于0时表示有事件就绪;返回值为0时表示超时;返回值为-1时表示出错,错误原因可以通过 `errno` 来获取。使用 `epoll_wait` 函数,可以等待指定的 epoll 实例上的事件就绪,并将就绪事件保存在 `events` 数组中。然后可以遍历 `events` 数组,对每个就绪事件进行相应的处理。
下面是一个使用 epoll 的 TCP 并发服务器的示例代码,使用 C 语言实现:
```c
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
int main() {
int server_fd, epoll_fd, client_fd, num_ready_events;
ssize_t num_bytes;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len;
char buffer[BUFFER_SIZE];
// 创建 TCP 套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket");
return 1;
}
// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8888);
// 绑定地址和端口
if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
return 1;
}
// 监听连接
if (listen(server_fd, SOMAXCONN) == -1) {
perror("listen");
return 1;
}
// 创建 epoll 实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
return 1;
}
// 将服务器套接字加入 epoll
struct epoll_event event;
event.events = EPOLLIN; // 监听可读事件
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("epoll_ctl");
return 1;
}
struct epoll_event events[MAX_EVENTS];
printf("Server is listening on port 8888...\n");
while (1) {
// 等待事件就绪
num_ready_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (num_ready_events == -1) {
perror("epoll_wait");
return 1;
}
// 处理就绪事件
for (int i = 0; i < num_ready_events; i++) {
if (events[i].data.fd == server_fd) {
// 服务器套接字有可读事件,表示有新连接请求
client_addr_len = sizeof(client_addr);
client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == -1) {
perror("accept");
continue;
}
// 将新客户端套接字加入 epoll
event.events = EPOLLIN; // 监听可读事件
event.data.fd = client_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
perror("epoll_ctl");
return 1;
}
printf("Accepted new connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
} else {
// 客户端套接字有可读事件,表示有数据到达
client_fd = events[i].data.fd;
// 读取客户端发送的数据
num_bytes = read(client_fd, buffer, BUFFER_SIZE);
if (num_bytes == -1) {
perror("read");
return 1;
} else if (num_bytes == 0) {
// 客户端关闭连接
printf("Client disconnected\n");
// 关闭并从 epoll 中移除客户端套接字
close(client_fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
} else {
// 打印接收到的数据
buffer[num_bytes] = '\0';
printf("Received data from client: %s\n", buffer);
// 回复相同的数据给客户端
if (write(client_fd, buffer, num_bytes) == -1) {
perror("write");
return 1;
}
}
}
}
}
// 关闭服务器套接字和 epoll 实例
close(server_fd);
close(epoll_fd);
return 0;
}
```
这个示例代码创建了一个 TCP 服务器,并使用 epoll 实现并发处理多个客户端连接。
accept 之后
扩展:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
端口复用最常见的用途:
防止服务器重启时之前绑定的端口还未释放
程序突然退出而系统没有释放端口
在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为,TCP连接没有完全断开指的是connfd(127.0.0.1:6666)没有完全断开,而我们重新监听的是lis-tenfd(0.0.0.0:6666),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
#include
#include
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
在server代码的socket()和bind()调用之间插入如下代码:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
5. 在多线程中,针对锁本身带来的影响,你怎么处理的?
在Linux网络编程中,多线程对锁的使用需要注意,因为锁本身的使用也会带来一定的开销,因此需要合理地处理锁的使用,以避免对性能的影响。
以下是一些处理锁带来影响的方式:
1. 减小锁的粒度:将锁的粒度缩小到最小,可以减少锁的竞争。比如,将一个大锁拆分为多个小锁,或在读写操作中采用读写锁来减小锁的粒度。
2. 避免死锁:死锁是多线程编程中常见的问题,需要编写代码时避免出现死锁。通常可以通过加锁顺序一致、设置锁超时时间、使用递归锁、避免锁的嵌套等方式来避免死锁。
3. 合理地分配锁的方式:在多线程程序中,当多个线程需要使用同一资源时,可以采用不同的锁来避免锁的竞争。比如,可以采用线程本地存储(TLS)的方式来避免线程间对锁的竞争,或者采用无锁同步方式等方式来实现线程安全。
4. 使用条件变量:在线程间的通信中,使用条件变量可以减少对锁的竞争,避免线程频繁地检查等待条件。
总之,在多线程编程中,需要注意合理地处理锁的使用,并选择合适的线程同步机制以避免对性能的影响。
提高锁的性能可以采用以下几种方法:
1. 减小锁的粒度:将锁的范围缩小到最小,避免对整个数据结构或大段代码进行加锁。通过减小锁的粒度,可以减少不必要的锁竞争,提高并发性能。例如,可以将一个大锁拆分为多个小锁,或者使用细粒度的读写锁。
2. 使用无锁数据结构:无锁数据结构是一种并发编程技术,通过使用原子操作等方式来实现数据的并发访问,避免了锁带来的竞争和开销。例如,使用原子操作指令来实现无锁队列、无锁哈希表等数据结构,可以提高并发性能。
3. 使用读写锁:如果读操作远远多于写操作,可以使用读写锁来提高性能。读写锁允许多个线程同时读取共享数据,但只允许一个线程写入共享数据。这样可以提高读取的并发性能,减少锁竞争。
4. 使用乐观锁(自旋锁):乐观锁将线程的竞争转化为对共享数据的版本检查,通过自旋等待共享资源变得可用,而不是立即阻塞。乐观锁适用于共享资源竞争不激烈的场景,可以减少线程阻塞和切换的开销。
5. 避免锁的嵌套:在编写多线程代码时,应尽量避免锁的嵌套,因为锁的嵌套会增加锁的竞争和开销。如果确实需要嵌套锁,可以考虑使用递归锁,在同一个线程中对同一个锁进行多次加锁。
6. 使用合适的同步原语:根据具体的需求,选择合适的同步原语来实现线程间的同步和协作。例如,条件变量、信号量、屏障等都可以在不同场景中提高锁的性能和效率。
7. 优化锁算法:对于一些常用的锁算法,有时可以通过调整参数或者使用更高效的算法来提高锁的性能。例如,自旋锁的自旋次数、锁的队列实现等都可以进行优化。
总的来说,提高锁的性能需要根据具体的应用场景,选择合适的锁策略和同步机制,并根据实际情况对锁的粒度、锁的嵌套、锁的算法等进行优化。同时,针对特定的共享资源,可以考虑使用无锁数据结构或其他并发编程技术来避免锁的竞争和开销,提高性能。