网络编程 26(上)阻塞 I/O 线程模型
目标
使用轻量级的线程处理多个连接,为每一个连接创建一个独立的线程去服务
一、POSIX 线程模型
POSIX 线程是现代 UINX 系统提供的处理线程的标准接口
1.1 创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数
- thread:线程 ID(tid),类型为 pthread_t,输出参数
- attr:线程属性(如:优先级、守护进程等),通过 pthread_attr_t 设置,一般置为 NULL
- start_routine:线程入口函数
- arg:线程入口函数结构体参数
返回值
- 0:成功
- 非 0:错误码
在线程入口函数中,调用 pthread_self()
函数获取线程 tid
pthread_t pthread_self(void);
1.2 终止线程
父线程终止所有子线程
父线程等待其它所有子线程终止,然后父线程终止
void pthread_exit(void *retval);
父线程终止某个子线程
父线程终止某个子线程
int pthread_cancel(pthread_t thread);
回收已终止线程资源
调用后主线程阻塞,直到子线程自然终止,不强迫子线程终止
int pthread_join(pthread_t thread, void **retval);
1.3 分离线程
分离线程不能被其他线程杀死或回收资源,当父线程不需要对每个子线程进行关闭时,可在线程入口函数一开始时将线程设置成分离的,当线程终止后会自动回收相关的线程资源
int pthread_detach(pthread_t thread);
二、阻塞 I/O 线程模型
服务端
thread_server.c
#include "common.h"
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) {
return c + 13;
} else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) {
return c - 13;
}
return c;
}
void loop_echo(int fd) {
char outbuf[512];
int i;
ssize_t result;
while (1) {
result = recv(fd, &outbuf, sizeof(outbuf), 0);
if (result == 0) {
break;
} else if (result == -1) {
error(1, errno, "recv failed");
break;
}
for (i = 0; i < result; i++) {
outbuf[i] = rot13_char(outbuf[i]);
if (outbuf[i] == '\n') {
send(fd, outbuf, result, 0);
break;
}
}
}
}
// 线程入口函数
void *thread_run(void *arg) {
// 设置线程成分离的
pthread_detach(pthread_self());
int fd = *((int *)arg);
loop_echo(fd);
}
int main(int argc, char **argv) {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
// 端口复用
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
socklen_t servlen = sizeof(servaddr);
int bind_rt = bind(listenfd, (struct sockaddr *)&servaddr, servlen);
if (bind_rt < 0) {
error(1, errno, "bind failed");
}
int listen_rt = listen(listenfd, LISTENQ);
if (listen_rt < 0) {
error(1, errno, "listen failed");
}
pthread_t tid;
while (1) {
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listenfd, (struct sockaddr *)&ss, &slen);
if (fd < 0) {
error(1, errno, "accept failed");
} else {
// 为每一个连接创建一个线程去服务
pthread_create(&tid, NULL, thread_run, &fd);
}
}
}
头文件 common.h
#ifndef CHAP_26_COMMON_H
#define CHAP_26_COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h> /* basic socket definitions */
#include <netinet/in.h> /* sockaddr_in{} and other Internet defns */
#include <arpa/inet.h> /* inet(3) functions */
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
void error(int status, int err, char *fmt, ...);
#define SERV_PORT 43211
#define LISTENQ 1024
#endif //CHAP_26_COMMON_H
三、CMake 管理当前项目
① 代码组成
-CMakeLists.txt
-include:存放头文件
-src:存放源代码
CMakeLists.txt
CMAKE_MINIMUM_REQUIRED(VERSION 3.1)
SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/include)
ADD_SUBDIRECTORY(src)
include 目录:include/common.h(common.h 上面有)
src 目录(thread_server.c 上面有)
src/CmakeLists.txt
ADD_EXECUTABLE(thread_server thread_server.c)
TARGET_LINK_LIBRARIES(thread_server pthread)
② 创建并进入 build 目录
mkdir build && cd build
③ 外部编译
cmake .. && make
四、测试
可以使用一个或多个 telnet 客户端连接服务器,检验交互是否正常
测试步骤
① 打开三个命令行窗口
② 其中一个窗口先执行服务器命令,输入命令 ./thread_server
后回车
③ 其余窗口执行客户端命令,输入命令 ./telnet-client 127.0.0.1 43211
后回车
左:服务端;右上、右下:客户端
上面的阻塞 I/O 线程模型的服务端程序,可以并发处理多个不同的客户端连接,互不干扰
总结
- 每一个进程都会产生一个线程,一般称主线程,主线程可以再产生子线程
- 在同一进程下,线程上下文切换的开销比进程要小得多
- 线程上下文切换
CPU 从一个计算场景切换到另一个计算场景,程序计数器、寄存器等值都要重新载入新场景的值,这个过程就是线程的上下文切换