多线程服务器
1、线程概念
1.1、为什么引入线程
为了实现服务端并发处理客户端请求,我们介绍了多进程模型、select和epoll,这三种办法各有优缺点。创建(复制)进程的工作本身会给操作系统带来相当沉重的负担。而且,每个进程有独立的内存空间,所以进程间通信的实现难度也会随之提高。
多进程模型的缺点概括如下:
- 创建进程的过程会带来一定的开销
- 为了完成进程间数据交换,需要特殊的IPC计数
- 每秒数十次、多则上千次的上下文切换是创建进程是最大的开销
且进程的切换同样也是不菲的开销。什么是进程切换?我们都知道计算机即便只有一个CPU也可以同时运行多个进程,这是因为系统将CPU时间分成多个微小的块后分配给多个进程,比如进程B在进程A之后执行,当进程A所分配的CPU时间到点之后,要开始执行进程B,此时需要将进程A的数据移出内存保存到磁盘,并读入进程B的数据,所以上下文切换需要比较长的时间,即使通过优化加快速度,也会存在局限。
为了保持多进程的优点,同时在一定程度上克服其缺点,人们引入了线程。这是为了将进程的各种劣势降至最低限度而设计的一种“轻量级进程”,线程相比进程有如下优点:
- 线程的创建和上下文切换比进程的创建和上下文切换更快
- 线程间交换数据时无需特殊技术
1.2、线程和进程的差异
每个进程的内存空间都由保存全局变量的“数据区”、向malloc等函数动态分配提供空间的堆(Heap)、函数运行时使用的栈(Stack)构成。每个进程都拥有这种独立的空间,多个进程结构如图1-1所示

图1-1 进程间独立的内存
但如果以获得多个代码执行流为主要目的,则不应像图1-1那样完全分离内存结构,而只需分离栈区域,通过这种方式可以获得如下优势:
- 上下文切换时不需要切换数据区和堆
- 可以利用数据区和堆交换数据
实际上这就是线程,线程为了保持多条代码执行流而隔开了栈区域,因此具有如图1-2所示的内存结构

图1-2 线程的内存结构
如图1-2所示,多个线程将共享数据区和堆,为了保持这种结构,线程将在进程内创建并运行。也就是说,进程和线程可以定义为如下形式:
- 进程:在操作系统构成单独执行流的单位
- 线程:在进程构成单独执行流的单位
如果说进程在操作系统内部生成多个执行流,那么线程就在同一进程内部创建多条执行流。因此,操作系统、进程、线程之间的关系可以通过图1-3表示

图1-3 操作系统、进程、线程之间的关系
2、线程的创建及运行
2.1、线程的创建和执行流程
线程具有单独的执行流,因此需要单独定义线程的main函数,还需要请求操作系统在单独的执行流中执行该函数,完成该功能的函数如下:
#include<pthread.h> int pthread_create(pthread_t * restrict thread, const pthread_attr_t * restrict attr, void* (* start_routine)(void *), void * restrict arg);//成功时返回0,失败时返回其他值
- thread:保存新创建线程ID的变量地址值,线程与进程相同,也需要用于区分不同线程的ID
- attr:用于传递线程属性的参数,传递NULL时,创建默认属性的线程
- start_routine:相当于线程的main函数的、在单独执行流中执行的函数地址值(函数指针)
- arg:通过第三个参数传递调用函数时包含传递参数信息的变量地址值
下面,我们来看一个示例
thread1.c
#include <stdio.h> #include <pthread.h> void *thread_main(void *arg); int main(int argc, char *argv[]) { pthread_t t_id; int thread_param = 5; if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0) { puts("pthread_create() error"); return -1; }; sleep(10); puts("end of main"); return 0; } void *thread_main(void *arg) { int i; int cnt = *((int *)arg); for (i = 0; i < cnt; i++) { sleep(1); puts("running thread"); } return NULL; }
- 第10行:请求创建一个线程,从thread_main函数调用开始,在单独的执行流中执行。同时在调用thread_main函数时向其传递thread_param变量的地址值
- 第15行:调用sleep函数使main函数停顿10秒,这是为了延迟进程的终止时间。执行第16行的return语句后终止进程,同时终止内部创建的线程。因此,为保证线程的正常执行而添加这条语句
- 第19、22行:传入arg参数的是第10行pthread_create函数的第四个参数
编译thread1.c并运行
# gcc thread1.c -o thread1 -lpthread # ./thread1 running thread running thread running thread running thread running thread end of main
从上述运行结果可以看到,线程相关代码在编译时需添加-lpthread选项声明需要连接线程库,只有这样才能调用头文件pthread.h中声明的函数,上述程序的执行流程如图1-4所示

图1-4 示例thread1.c的执行流程
图1-4中的虚线代表执行流程,向下的箭头指的是执行流,横向箭头是函数调用。
接下来,可以尝试将上述示例的第15行sleep函数的调用语句改为sleep(2)。运行之后大家会发现不会再像之前那样打印5次"running thread"字符串。因为main函数返回后整个进程将被销毁,如图1-5所示

图1-5 终止进程和线程
正因如此,我们之前的示例中通过调用sleep函数向线程提供了充足的时间 。
那么,如果我们希望等线程执行完毕,再结束程序,是不是一定要调用sleep函数?如果是,那么又牵扯出一个问题了,线程是在何时执行完毕呢?并非所有的程序都像thread1.c一样可预测线程的执行时间。那么,为了等待线程执行完毕,难道我们要用一个非常大的数作为sleep的参数吗?那这样就算线程可以执行完,程序依然在休眠,造成计算机资源的浪费是一定的。那么,针对这一困境,是否有解决方案呢?当然是有的,那就是pthread_join函数 :
#include <pthread.h> int pthread_join(pthread_t thread, void ** status);//成功时返回0,失败时返回其他值
- thread: thread所对应的线程终止后才会从pthread_join函数返回,换言之调用该函数后当前线程会一直阻塞到thread对应的线程执行完毕后才返回
- status:保存线程的main函数返回值的指针变量地址值
thread2.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> void *thread_main(void *arg); int main(int argc, char *argv[]) { pthread_t t_id; int thread_param = 5; void *thr_ret; if (pthread_create(&t_id, NULL, thread_main, (void *)&thread_param) != 0) { puts("pthread_create() error"); return -1; }; if (pthread_join(t_id, &thr_ret) != 0) { puts("pthread_join() error"); return -1; }; printf("Thread return message: %s \n", (char *)thr_ret); free(thr_ret); return 0; } void *thread_main(void *arg) { int i; int cnt = *((int *)arg); char *msg = (char *)malloc(sizeof(char) * 50); strcpy(msg, "Hello, I'am thread~ \n"); for (i = 0; i < cnt; i++) { sleep(1); puts("running thread"); } return (void *)msg; }
- 第19行:main函数中,针对第13行创建的线程调用pthread_join函数,因此,main函数将等待ID保存在t_id变量中的线程终止
- 第11、19、41行:第41行返回的值将保存到第19行第二个参数thr_ret。需要注意的是,该返回值是thread_main函数内部动态分配的内存空间地址值
编译thread2.c并运行
# gcc thread2.c -o thread2 -lpthread # ./thread2 running thread running thread running thread running thread running thread Thread return message: Hello, I'am thread~
接下来我们来看thread2.c的执行流程图,如图1-6所示

图1-6 调用pthread_join函数
2.2、可在临界区内调用的函数
之前的示例只创建一个线程,接下来的示例将创建多个线程。当然,无论创建多少个线程,其创建方法没有区别。但关于线程的运行需要考虑“多个线程同时调用函数时(执行时)可能产生的问题”。这类函数内部存在临界区,也就是说,多个线程同时执行这部分代码时,可能引起问题。根据临界区是否引起问题,函数可分为两类:
- 线程安全函数
- 非线程安全函数
线程安全函数被多个线程同时调用不会发生问题,反之,非线程安全函数被调用时就会出现问题。
下面我们介绍一个示例,将计算1到10的和,但并不是在main函数中计算,而是创建两个线程,其中一个线程计算1到5的和,另一个线程计算6到10的和,main函数只负责输出结果。这种方式的编程模型称为“工作线程模型”。计算1到5之和与计算6到10之和的线程将成为main线程管理的工作。最后,在给出示例代码之前先给出程序执行流程图,如图1-7所示

图1-7 示例thread3.c的执行流程
thread3.c
#include <stdio.h> #include <pthread.h> void *thread_summation(void *arg); int sum = 0; int main(int argc, char *argv[]) { pthread_t id_t1, id_t2; int range1[] = {1, 5}; int range2[] = {6, 10}; pthread_create(&id_t1, NULL, thread_summation, (void *)range1); pthread_create(&id_t2, NULL, thread_summation, (void *)range2); pthread_join(id_t1, NULL); pthread_join(id_t2, NULL); printf("result: %d \n", sum); return 0; } void *thread_summation(void *arg) { int start = ((int *)arg)[0]; int end = ((int *)arg)[1]; while (start <= end) { sum += start; start++; } return NULL; }
这里要注意一下,两个线程都访问全局变量sum
编译thread3.c 并运行
# gcc thread3.c -o thread3 -lpthread # ./thread3 result: 55
运行结果是55,虽然正确,但示例本身存在问题。此处存在临界区相关问题,因此再介绍另一示例,该示例与上述示例相似,只是增加了发生临界区相关错误的可能性
thread4.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> #define NUM_THREAD 100 void *thread_inc(void *arg); void *thread_des(void *arg); long long num = 0; int main(int argc, char *argv[]) { pthread_t thread_id[NUM_THREAD]; int i; printf("sizeof long long: %d \n", sizeof(long long)); for (i = 0; i < NUM_THREAD; i++) { if (i % 2) pthread_create(&(thread_id[i]), NULL, thread_inc, NULL); else pthread_create(&(thread_id[i]), NULL, thread_des, NULL); } for (i = 0; i < NUM_THREAD; i++) pthread_join(thread_id[i], NULL); printf("result: %lld \n", num); return 0; } void *thread_inc(void *arg) { int i; for (i = 0; i < 50000000; i++) num += 1; return NULL; } void *thread_des(void *arg) { int i; for (i = 0; i < 50000000; i++) num -= 1; return NULL; }
上述示例共创建100个线程,其中一半执行thread_inc函数中的代码,另一半则执行thread_des函数中的代码,全局变量sum经过增减后的值应还是0,但是,我们在编译执行下程序
# gcc thread4.c -o thread4 -lpthread # ./thread4 sizeof long long: 8 result: 10862532
可以看到,结果并非我们预想的那样。虽然暂时不清楚原因,但可以肯定,冒然使用线程对变量进行操作,是有可能发生问题的。那么,这是什么问题?如何解决,我们会在下面介绍。
2.3、线程存在的问题和临界区
前面的thread4.c中,我们发现多线程对同一变量进行加减,最后的结果居然不是我们预料之内的。其实,如果多执行几次程序,会发现每次程序计算的结果都不一样。那么,造成这样的原因是什么呢?
现在,假设我们一个全局变量sum的值为99,我们创建两个线程,要对sum进行加1操作,那么理想情况下,sum的值应为101。那么要对sum加1并赋值给sum,我们可以简写为:sum+=1。于是多线程可以开始对sum进行操作,但因为是多线程,有可能在一个进程内,一个线程还没执行完,另外一个线程得到CPU时间开始执行。
所以,让我们把目光放回:sum+=1这行代码,这段代码其实有两个动作,第一个动作是sum+1,第二个是将之前相加的结果重新赋值给sum。那么,有两个线程并发给sum加1,有可能第一个线程执行完相加的结果,得到100,但在赋值之前便失去了CPU时间,轮到另外一个线程获得CPU时间,执行加1的操作,等到执行完相加的操作,第二个线程的CPU时间到头了,最后,两个线程都要执行赋值操作,最后我们看到sum只有100,而并不是我们预想中的101。
2.3.1、线程同步
为了要解决这一问题,我们必须要求在从sum加1到完成sum的赋值这段临界区,只能有一个线程来完成。而就涉及到线程同步了,这里有两种技术可以实现线程同步,分别是“互斥量”(Mutex)和“信号量”(Semaphore)
(1)、互斥量
互斥量是“Mutual Exclusion”的简写,表示不允许多个线程同时访问。举个例子,临界区就好比是小房间里的取款机,现在大部分取款机都会装在一个小房间,当需要取款时一个一个人按顺序进入小房间,栓上门开始取款,取款完毕后离开小房间,让下一个人进来取款。把取款的人当做线程,如果多人同时进入小房间(临界区)那肯定会造成安全上的问题,如账号密码泄露。因此,门栓就是互斥量,当栓上门时,不允许其他人(线程)进入小房间(临界区)进行操作。
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t * attr); int pthread_mutex_destroy(pthread_mutex_t * mutex); //成功时返回0,失败时返回其他值
- mutex:创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值
- attr:传递即将创建的互斥量属性,没有特别需要指定的属性时传递NULL
从上述函数声明中可以看出,为了创建相当于锁系统的互斥量,需要声明如下pthread_mutex_t型变量
pthread_mutex_t mutex;
该变量的地址将传递给pthread_mutex_init函数,用来保存操作系统创建的互斥量(锁系统)。调用pthread_mutex_destroy函数时同样需要该信息,如果不需要配置特殊的互斥量属性,则向第二个参数传递NULL时,可以利用PTHREAD_MUTEX_INITALIZER宏进行如下声明:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
推荐使用pthread_mutex_init函数进行初始化,因为通过宏进行初始化时很难发现发生的错误。接下来介绍利用互斥量锁住或释放临界区时使用的函数
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t * mutex); int pthread_mutex_unlock(pthread_mutex_t * mutex); //成功时返回0,失败时返回其他值
通过函数名我们很容易理解函数的作用,进入临界区前调用pthread_mutex_lock,调用该函数时,如果发现有其他线程已进入临界区,则pthread_mutex_lock函数会陷入阻塞,直到进入里面的线程调用pthread_mutex_unlock函数退出临界区为止
mutex.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> #define NUM_THREAD 100 void *thread_inc(void *arg); void *thread_des(void *arg); long long num = 0; pthread_mutex_t mutex; int main(int argc, char *argv[]) { pthread_t thread_id[NUM_THREAD]; int i; pthread_mutex_init(&mutex, NULL); for (i = 0; i < NUM_THREAD; i++) { if (i % 2) pthread_create(&(thread_id[i]), NULL, thread_inc, NULL); else pthread_create(&(thread_id[i]), NULL, thread_des, NULL); } for (i = 0; i < NUM_THREAD; i++) pthread_join(thread_id[i], NULL); printf("result: %lld \n", num); pthread_mutex_destroy(&mutex); return 0; } void *thread_inc(void *arg) { int i; pthread_mutex_lock(&mutex); for (i = 0; i < 50000000; i++) num += 1; pthread_mutex_unlock(&mutex); return NULL; } void *thread_des(void *arg) { int i; for (i = 0; i < 50000000; i++) { pthread_mutex_lock(&mutex); num -= 1; pthread_mutex_unlock(&mutex); } return NULL; }
- 第11行:声明了保存互斥量读取值的变量,之所以声明全局变量是因为thread_inc函数和thread_des函数都需要访问互斥量
- 第32行:销毁互斥量,不需要互斥量时应销毁
- 第39、42行:实际临界区只是第41行,但此处连同第40行的循环语句一起用作临界区,调用了lock、unlock函数
- 第50、52行:通过lock、unlock函数围住对应于临界区的第51行语句
编译mutex.c并运行
# gcc mutex.c -D_REENTRANT -o mutex -lpthread # ./mutex result: 0
从运行结果可以看出,已解thread4.c的问题。但确认运行结果需要等待比较长的时间,因为互斥量lock、unlock函数的调用过程耗时较久。首先 ,分析一下thread_inc函数的同步过程
void *thread_inc(void *arg) { int i; pthread_mutex_lock(&mutex); for (i = 0; i < 50000000; i++) num += 1; pthread_mutex_unlock(&mutex); return NULL; }
以上临界区划分范围较大,但可以最大限度减少互斥量lock、unlock函数的调用次数,上述示例中,thread_des函数比thread_inc函数多调用49,999,999次互斥量lock、unlock函数;但是thread_inc相比于thread_des也不是全无缺点,因为当循环完成之前,不允许任何线程访问
(2)、互斥量
#include <semaphore.h> int sem_init(sem_t * sem, int pshared, unsigned int value); int sem_destroy(sem_t * sem); //成功时返回0,失败时返回其他值
- sem:创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值
- pshared:传递其他值时,创建可由多个进程共享的信号量;传递0时,创建只允许一个进程内部使用的信号量,我们需要完成同一进程内的线程同步,故传0
- value:指定新创建的信号量初始值
上述函数的pshared参数超出我们关注的范围,故默认向其传递0。稍后讲解通过value参数初始化的信号量值是多少,接下来介绍信号量中相当于互斥量lock、unlock的函数
#include <semaphore.h> int sem_wait(sem_t *sem); int sem_post(sem_t *sem);
- sem:传递保存信号量读取值的变量地址值,传递给sem_post时信号量加1,传递给sem_wait时信号量减1
调用sem_init函数时,操作系统将创建信号量对象,此对象中记录着“信号量值”(整数)。该值在调用sem_post函数时加1,调用sem_wait函数时减1。但信号量的值不能小于0。因此,在信号量为0的情况下调用sem_wait函数时,调用函数的线程将进入阻塞状态,如果此时有其他线程函数调用sem_post函数,信号量的值将变为1,而原本阻塞的线程可以将该信号量重新减为0并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区
sem_wait(&sem); //临界区开始 //…… //临界区结束 sem_post(&sem);
上述代码结构中,调用sem_wait函数进入临界区的线程在调用sem_post函数前不允许其他线程进入临界区。信号量的值在0和1之间跳转。因此,具有这种特性的机制称为“二进制信号量”
semaphore.c
#include <stdio.h> #include <pthread.h> #include <semaphore.h> void *read(void *arg); void *accu(void *arg); static sem_t sem_one; static sem_t sem_two; static int num; int main(int argc, char *argv[]) { pthread_t id_t1, id_t2; sem_init(&sem_one, 0, 0); sem_init(&sem_two, 0, 1); pthread_create(&id_t1, NULL, read, NULL); pthread_create(&id_t2, NULL, accu, NULL); pthread_join(id_t1, NULL); pthread_join(id_t2, NULL); sem_destroy(&sem_one); sem_destroy(&sem_two); return 0; } void *read(void *arg) { int i; for (i = 0; i < 5; i++) { fputs("Input num: ", stdout); sem_wait(&sem_two); scanf("%d", &num); sem_post(&sem_one); } return NULL; } void *accu(void *arg) { int sum = 0, i; for (i = 0; i < 5; i++) { sem_wait(&sem_one); sum += num; sem_post(&sem_two); } printf("Result: %d \n", sum); return NULL; }
- 第14、15行:生成两个信号量,一个信号量的值为0,另一个为1
- 第35、48行:利用信号量变量sem_two调用wait函数和post函数,这是为了防止在调用accu函数的线程还未取走数据的情况下,调用read函数的线程覆盖原值
- 第37、46行:利用信号量变量sem_one调用wait和post函数,这是为了防止调用read函数的线程写入新值之前,accu函数再取走旧的数据
编译semaphore.c并运行
# gcc semaphore.c -D_REENTRANT -o semaphore -lpthread # ./semaphore Input num: 1 Input num: 2 Input num: 3 Input num: 4 Input num: 5 Result: 15
2.3.2、线程的销毁和多线程并发服务端的实现
Linux线程并不是在首次调用的线程main函数返回时自动销毁,所以用如下两种方法之一加以明确,否则由线程创建的内存空间将一直存在
- 调用pthread_join函数
- 调用pthread_detach函数
之前调用过pthread_join函数,调用该函数时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程将进入阻塞状态。因此,通常通过如下函数调用引导线程销毁
#include <pthread.h> int pthread_detach(pthread_t thread);//成功时返回0,失败时返回其他值
- thread:终止的同时需要销毁的线程ID
调用上述函数不会引起线程终止或进入阻塞状态,可以通过该函数引导销毁线程创建的内存空间。调用该函数后不能针对相应线程调用pthread_join函数,需要注意一下。
多线程并发服务端的实现
chat_serv.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h> #include <pthread.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 *msg); int clnt_cnt = 0; int clnt_socks[MAX_CLNT]; pthread_mutex_t mutx; int main(int argc, char *argv[]) { int serv_sock, clnt_sock; struct sockaddr_in serv_adr, clnt_adr; int clnt_adr_sz; pthread_t t_id; if (argc != 2) { printf("Usage : %s <port>\n", argv[0]); exit(1); } pthread_mutex_init(&mutx, NULL); serv_sock = socket(PF_INET, SOCK_STREAM, 0); 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); 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("Connected 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, 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++) // remove disconnected client { 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; } void send_msg(char *msg, int len) // send to all { 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 *msg) { fputs(msg, stderr); fputc('\n', stderr); exit(1); }
- 第17、18行:用于管理接入的客户端套接字的变量和数组,访问这两个变量的代码将构成临界区
- 第51行:每当有新连接时,将相关信息写入变量clnt_cnt和clnt_socks
- 第54行:创建线程向新接入的客户端提供服务,由该线程执行第62行定义的函数
- 第55行:调用pthread_detach函数从内存中完全销毁已终止的线程
- 第86行: 该函数负责向所连接的客户端发送信息
chat_clnt.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <pthread.h> #define BUF_SIZE 100 #define NAME_SIZE 20 void *send_msg(void *arg); void *recv_msg(void *arg); void error_handling(char *msg); char name[NAME_SIZE] = "[DEFAULT]"; char msg[BUF_SIZE]; int main(int argc, char *argv[]) { int sock; struct sockaddr_in serv_addr; 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); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) error_handling("connect() error"); 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 *send_msg(void *arg) // send thread main { 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) // read thread main { 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; } void error_handling(char *msg) { fputs(msg, stderr); fputc('\n', stderr); exit(1); }
编译chat_serv.c并运行
# gcc chat_serv.c -D_REENTRANT -o chat_serv -lpthread # ./chat_serv 8500 Connected client IP: 127.0.0.1 Connected client IP: 127.0.0.1
编译chat_clnt.c并运行
# gcc chat_clnt.c -D_REENTRANT -o chat_clnt -lpthread # ./chat_clnt 127.0.0.1 8500 Sam Hi everyone~ [Sam] Hi everyone~ [Amy] Hi Sam! Hello Amy! [Sam] Hello Amy!
# ./chat_clnt 127.0.0.1 8500 Amy [Sam] Hi everyone~ Hi Sam! [Amy] Hi Sam! [Sam] Hello Amy!

浙公网安备 33010602011771号