网络编程笔记(六)-标准IO、epoll、多线程
网络编程笔记(六)-标准IO、epoll、多线程
参考《TCP/IP 网络编程》15、16、17、18 章
套接字和标准 I/O
标准 I/O 函数的 2 个优点:
- 良好的移植性(Portability)。
- 利用缓冲提高性能。
使用 read 和 write 函数传输 400M 文件的时间远远大于使用标准函数 fgets 和 fputs。
标准 I/O 函数的几个缺陷:
-
不容易进行双向通信。如果要同时进行读写操作,则应以 r+、w+、a+ 模式打开。
-
有时候可能频繁调用 fflush。每次切换读写工作状态都应该调用 fflush 函数。
-
需要以 FILE 结构体指针形式返回文件描述符;
利用 fdopen 函数将文件描述符转换成 FILE 结构体指针:
#include <stdio.h>
/*
fildes: 需要转换的文件描述符
mode: 将要创建的 FILE 结构体指针的模式信息
*/
// 成功时返回转换的FILE结构体指针,失败时返回 NULL
FILE * fdopen(int fildes, const char *mode);
利用 fileno 函数将 FILE 结构体指针转换成文件描述符
#include <stdio.h>
// 成功时返回转换后的文件描述符,失败时返回-1
int fileno(FILE * stream);
关于 I/O 分离流的其他内容
I/O 分离流的方式
I/O 流分离的方式:
- TCP I/O 过程(routine)分离:通过调用 fork 函数复制出一个文件描述符,以区分输入和输出中使用的文件描述符,虽然文件描述符不会根据输入和输出进行区分,但是我们分开了 2 个文件描述符的用途。
- 调用 2 次调用 fdopen:创建读模式 FILE 指针和写模式 FILE 指针,分离了输入工具和输出工具。
TCP I/O 过程(routine)分离流的好处:
-
通过分开输入过程(代码)和输出过程降低实现难度。
-
与输入无关的输出操作可以提高速度。
2 次调用 fdopen 分离流的目的:
-
为了将 FILE 指针按读模式、写模式加以区分。
-
可以通过区分读写模式降低难度。
-
通过区分 I/O 缓冲提高缓冲性能。
流分离带来的问题
流分离带来的问题:终止流时无法半关闭。读模式 FILE 指针、写模式 FILE 指针都是基于同一个文件描述符创建的。因此,针对任意一个 FILE 指针调用 fclose 都将关闭文件描述符,终止套接字。
解决方法:在创建 FILE 指针前复制文件描述符,然后利用各自的文件描述符创建读模式 FILE 指针和写模式 FILE 指针,这样销毁所有文件描述符后才能销毁套接字(引用计数)。
文件描述符的复制
dup 和 dup2 函数
#include <unistd.h>
int dup(int fildes)
// 成功时返回复制的文件描述符,失败时返回-1
/*
fildes:需要复制的文件描述符
filders2:明确指定的文件描述符整数值
*/
int dup2(int fildes, int fildes2)
无论复制出多少文件描述符,均应调用 shutdown 函数发送 EOF 并进入半关闭状态。
优于 select 的 epoll
select 和 epoll 的比较
select 不适合以 web 服务器端开发为主流的现代开发环境,所以要学习 Linux 平台下的 epoll。
基于 select 的 I/O 复用技术速度慢的原因:
-
调用 select 函数后常见的针对所有文件描述符的循环语句。
-
每次调用 select 函数时都需要向函数传递监视对象信息。
select 的优点:
-
服务器端接入者少。
-
程序具有兼容性——大部分操作系统都支持 select 函数。
epoll 函数
epoll_create 函数
epoll_create:向操作系统请求创建保存 epoll 文件描述符的空间,对应 select 方式下声明的 fd_set 变量。该函数返回的文件描述符主要用于区分 epoll 例程。
#include <sys/epoll.h>
// size: epoll实例的大小
// 返回值:成功时返回epoll文件描述符,失败时返回-1
int epoll_create(int size)
注意,size 只是建议 epoll 例程大小,仅供操作系统参考。Linux 2.6.8 之后的内核将完全忽略 size 参数,因为内核会根据情况调整 epoll 例程的大小。
epoll_ctl 函数
epoll_ctl:向空间(对应于 select 中的位数组)注册并注销文件描述符,对应 select 中的 FD_SET、FD_CLR 函数。
#include <sys/epoll.h>
/*
epfd: 用于注册监视对象的epoll例程的文件描述符。
op: 用于指定监视对象的添加、删除或更改。
fd: 需要注册的监视对象文件描述符。
event: 监视对象事件类型。
*/
// 成功时返回0,失败时返回-1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
// 用法:
struct epoll_event event;
event.events = EPOLLIN; // 详见结构体 epoll_event
event.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
举例:
epoll_ctl(A, EPOLL_CTL_ADD, B, C);
“epoll 例程中 A 注册文件描述符 B,主要目的是监视参数 C 中的事件。epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);
”从 epoll 例程 A 中删除文件描述符 B。“
op 操作:
- EPOLL_CTL_ADD:将文件描述符注册到 epoll 例程。
- EPOLL_CTL_DEL:从 epoll 例程中删除文件描述符。使用时需向第四个参数传递 NULL。
- EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况。
epoll_wait 函数
epoll_wait:等待文件描述符发生变化,类似 select 函数。
#include <sys/epoll.h>
/*
epfd: 标识事件发生监视范围epoll例程的文件描述符。
events: 保存发生事件的文件描述符集合的结构体地址值。
maxevents: 第二个参数中可以保存的最大事件数。
timeout: 以毫秒为单位的等待时间,传递-1时,一直等待直到发生事件。
*/
// 返回值:成功时返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。失败时返回-1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
结构体 epoll_event 和 epoll_data
select 函数中通过 fd_set 变量查看事件发生与否,epoll 通过以下结构体将发生事件的文件描述符集中到一起:
//epoll将发生事件的文件描述符集中在一起,放在epoll_event结构体中
struct epoll_event{
__uint32_t events;
epoll_data_t data;
}
typedef union epoll_data{
void * ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
声明足够大的 epoll_event 结构体数组后,传递给 epoll_wait 函数时,发生变化的文件描述符信息将被填入该数组,无需像 select 一样对所有文件描述符进行循环。
epoll_event 成员 epoll_events 中可以保存的常量及所指的事件类型:
基于 epoll 的回声服务器端
注意与 select 进行对比。
// echo_epollserv.c
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>
#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *message);
int main(int argc, char *argv[]) {
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
char buf[BUF_SIZE];
struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt;
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1) {
error_handling("socket error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) {
error_handling("bind() error");
}
if (listen(serv_sock, 5) == -1) {
error_handling("listen() error");
}
epfd = epoll_create(EPOLL_SIZE); //创建epoll例程
ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
// 将serv_sock添加到例程空间中
event.events = EPOLLIN;
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
while (1) {
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if (event_cnt == -1) {
puts("epoll_wait() error!");
break;
}
for (i = 0; i < event_cnt; i++) {
if (ep_events[i].data.fd == serv_sock) {
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
event.events = EPOLLIN;
event.data.fd = clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); //将请求连接的套接字添加到epoll例程中
printf("connected client : %d \n", clnt_sock);
} else { // read message
str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
if (str_len == 0) { // close request!
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); // 将断开连接的套接字从epoll例程中移除
close(ep_events[i].data.fd);
printf("closed client %d \n", ep_events[i].data.fd);
} else {
write(ep_events[i].data.fd, buf, str_len); // echo!
}
}
}
}
close(serv_sock);
close(epfd); // 注意关闭epoll例程
return 0;
}
void error_handling(char *message) {
fputs(message, stderr);
fputs("\n", stderr);
exit(1);
}
边缘触发和条件触发
边缘触发和条件触发的区别——在于发生事件的时间点:
- 条件触发:只要输入缓冲有数据,就会一直通知该事件。
- 边缘触发:输入缓冲收到数据时,仅注册一次事件。
epoll 默认以条件触发方式工作,select 也是以条件触发方式工作的。
实现边缘触发服务器的原理:通过 errno 变量验证错误原因;使用 fcntl 函数更改套接字选项,添加非阻塞 O_NONBLOCK 表至,完成非阻塞 I/O。
void setnonblockingmode(int fd){
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
边缘触发的优点:可以分离接收数据和处理数据的时间点。
多线程服务器端的实现
pthread_create 和 pthread_join
-
pthread_create
功能:生成线程
必须在调用此函数前就为 pthread_t 对象分配内存空间(malloc)
thread_handles = malloc(thread_countsizeof(pthread_t));
技巧:为每一个线程赋予唯一的 int 型参数 rank
pthread_create(&thread_handles[thread], NULL, Hello, (void) thread);
thread_p 是指针! -
pthread_join
分支合并(join)到主线程的直线中
pthread_join(thread_handles[thread], NULL);
thread_p 是值!类似的函数有 int pthread_detach(pthread_t thread); 调用 pthread_detach 不会引起线程终止或进入阻塞状态。
临界区
-
临界区
实例:计算 pi 的线程函数
竞争条件
当多个线程都要访问共享变量或共享文件这样的共享资源时,如果其中一个访问是更新操作,那么这些访问就可能会导致某种错误
临界区
一个更新共享资源的代码段,一次只允许一个线程执行该代码段
必须串行执行临界区的代码 -
互斥量(mutex)
特殊类型 pthread_mutex_t
初始化和销毁
pthread_mutex_int
pthread_mutex_init(&mutex, NULL);
pthread_mutex_destroy
pthread_mutex_destroy(&mutex);
上锁和解锁
pthread_mutex_lock
pthread_mutex_lock(&mutex);
pthread_mutex_unlock
pthread_mutex_unlock(&mutex);
使用互斥量时,多个进程进入临界区的顺序是随机的 -
信号量(semaphore)
信号量定义:一种特殊类型的 unsigned int 无符号整型变量,可以赋值为 0、1、2……
优点
能够初始化为任何非负值
信号量没有归属权,任何线程都能够对锁上的信号量进行解锁
生产者-消费者同步
一个消费者线程在继续运行前,需要等待一些条件或数据被生产者线程创建
基本操作
引入 semaphore 头文件
和 pthread_t 一样,init 前需要分配内存
semaphores = malloc(thread_count*sizeof(sem_t));
初始化和销毁
sem_init
sem_destroy
P 操作和 V 操作
sem_wait
将信号量减 1
如果值变成 负数,则阻塞执行 P 操作的线程,否则线程继续执行
sem_post
将信号量加 1
如果值 小于等于零,则唤醒一个等待进程
多线程并发服务器端的实现(聊天程序)
服务器:可以同时连接多个客户
#include <arpa/inet.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#define BUF_SIZE 100
#define MAX_CLNT 256
void *handle_clnt(void *arg);
void send_msg(char *msg, int len);
void error_handling(char *message);
int clnt_cnt = 0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx;
int main(int argc, char *argv[]) {
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_adr;
struct sockaddr_in clnt_adr;
int clnt_adr_sz;
pthread_t t_id; // 传递给线程的id
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
pthread_mutex_init(
&mutx, NULL); // 互斥访问临界区——clnt_cnt和clnt_socks
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1) {
error_handling("socket() error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) {
error_handling("bind() error");
}
if (listen(serv_sock, 5) == -1) {
error_handling("listen() error");
}
while (1) {
clnt_adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
// 以下代码访问临界区,将相关信息写入clnt_cnt和clnt_socks。
pthread_mutex_lock(&mutx);
clnt_socks[clnt_cnt++] = clnt_sock;
pthread_mutex_unlock(&mutx);
pthread_create(&t_id, NULL, handle_clnt, (void *)&clnt_sock);
pthread_detach(t_id); // 从内存中销毁完全已终止的线程
printf("Connetcted client IP :%s \n", inet_ntoa(clnt_adr.sin_addr));
}
close(serv_sock);
return 0;
}
void *handle_clnt(void *arg) {
int clnt_sock = *((int *)arg); // 传递给线程的参数
int str_len = 0;
int i;
char msg[BUF_SIZE];
while ((str_len = read(clnt_sock, msg, sizeof(msg))) != 0)
send_msg(msg, str_len);
pthread_mutex_lock(&mutx); // 访问临界区前获得锁
for (i = 0; i < clnt_cnt; i++) {
if (clnt_sock == clnt_socks[i]) { // 覆盖删除
while (i++ < clnt_cnt - 1) clnt_socks[i] = clnt_socks[i + 1];
break;
}
}
clnt_cnt--;
pthread_mutex_unlock(&mutx);
close(clnt_sock);
return NULL;
}
// send to all
void send_msg(char *msg, int len) {
int i;
pthread_mutex_lock(&mutx);
for (i = 0; i < clnt_cnt; i++) {
write(clnt_socks[i], msg, len);
}
pthread_mutex_unlock(&mutx);
}
void error_handling(char *message) {
fputs(message, stderr);
fputs("\n", stderr);
exit(1);
}
客户端:分离输入和输出而创建线程
#include <arpa/inet.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define BUF_SIZE 100
#define NAME_SIZE 20
void *send_msg(void *arg);
void *recv_msg(void *arg);
void error_handling(char *message);
char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];
int main(int argc, char *argv[]) {
int sock;
struct sockaddr_in serv_adr;
pthread_t snd_thread, rcv_thread;
void *thread_return;
if (argc != 4) {
printf("Usage : %s <IP> <port> <name>\n", argv[0]);
exit(1);
}
sprintf(name, "[%s]", argv[3]);
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1) {
error_handling("socket() error");
}
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) {
error_handling("connect() error\r\n");
} else {
printf("connect to the server!\n");
}
pthread_create(&snd_thread, NULL, send_msg, (void *)&sock);
pthread_create(&rcv_thread, NULL, recv_msg, (void *)&sock);
pthread_join(snd_thread, &thread_return);
pthread_join(rcv_thread, &thread_return);
close(sock);
return 0;
}
void error_handling(char *message) {
fputs(message, stderr);
fputs("\n", stderr);
exit(1);
}
void *send_msg(void *arg) {
int sock = *((int *)arg);
char name_msg[NAME_SIZE + BUF_SIZE];
while (1) {
fgets(msg, BUF_SIZE, stdin);
if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n")) {
close(sock);
exit(0);
}
sprintf(name_msg, "%s %s", name, msg);
write(sock, name_msg, strlen(name_msg));
}
return NULL;
}
void *recv_msg(void *arg) {
int sock = *((int *)arg);
char name_msg[NAME_SIZE + BUF_SIZE];
int str_len;
while (1) {
str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1);
if (str_len == -1) {
return (void *)-1;
}
name_msg[str_len] = 0;
fputs(name_msg, stdout);
}
return NULL;
}