TCP/IP 网络编程(十七)---多线程服务器端的实现

78 阅读20分钟

进程与线程

(一)进程的不足

之前在 TCP/IP 网络编程(九) 中介绍了多进程服务器端的实现方法。多进程模型与 selectepoll 相比的确有自身的优点,但同时也有缺点

① 创建进程的过程会带来一定的开销。

② 为了完成进程间的数据交换,需要特殊的 IPC(进程间通信) 技术。

但相比于下面的缺点,上述2个缺点不算什么:

“每秒少则数十次,多则数千次的 ‘上下文切换’ (Context Switching)是创建进程时最大的开销。”

什么是上下文切换?

运行程序前需要将相应进程信息读入内存,如果运行进程 A 之后需要紧接着运行进程 B,就应该将进程 A 相关信息移出内存,并读入进程 B 相关信息。这就是上下文切换。

(二)线程相对于进程的优点

为了保持多进程的优点,同时在一定程度上克服其缺点,线程(Thread)便出现了。这是为了将进程的各种劣势降至最低限度(不是直接消除)而设计的一种 “轻量级进程”。线程相比于进程具有如下优点:

① 线程的创建和上下文切换比进程的创建和上下文切换更快。

② 线程间交换数据时无需特殊技术。

(三)进程与线程的差异

创建进程是一个复制的过程,为了得到多条代码执行流将复制整个内存区域。

每个进程的内存空间都由保存全局变量的 “数据区”、向 malloc 等函数的动态分配提供空间的堆(Heap)、函数运行时使用的栈(Stack)构成。每个进程都拥有这种独立空间。多个进程的内存结构如下图所示:

image.png

如果以获得多个代码执行流为主要目的,则不应该像上图那样完全分离内存结构,而只需分离栈区域。通过这种方式可以获得如下优势:

① 上下文切换时不需要切换数据区和堆。

② 可以利用数据区和堆交换数据。

实际上这就是线程。线程为了保持多条代码执行流而隔开了栈区域,所以它的内存结构如下图所示:

image.png

多个线程将共享数据区和堆。为了保持这种结构,线程将在进程内创建并运行。即:

  • 进程:在操作系统构成单独执行流的单位。
  • 线程:在进程构成单独执行流的单位。

操作系统、进程、线程之间的关系可以通过下图表示:

image.png

线程创建及运行

(一)线程的创建和执行流程

线程具有单独的执行流,因此需要单独定义线程的 main 函数,还需要请求操作系统在单独的执行流中执行该函数,完成该功能的函数如下:

(1)pthread_create 函数

#include <pthread.h>

// 成功时返回0,失败时返回其他值
int pthread_create(pthread_t * restrict thread, 
                   const pthread_attr_t * restrict attr, 
                   void *(*start_routine)(void *), 
                   void * restrict arg);
  • restrict 关键字:

    • restrict是C99标准引入的关键字,用于指针。它告诉编译器,这个指针是唯一的、直接指向它所引用的对象的访问方式。也就是说,通过这个指针操作的数据不会通过其他指针修改或访问。
  • pthread_t * restrict thread:

    • 一个指向pthread_t类型变量的指针,用于存储创建的线程的标识符。restrict表示这个指针在函数的生命周期内是唯一访问此内存的方式。
  • const pthread_attr_t * restrict attr:

    • 一个指向线程属性的指针,用于定义新线程的属性。如果是NULL,则使用默认的线程属性。restrict表示在调用pthread_create时,该指针不会与其他指针指向相同的内存区域。
  • void *(*start_routine)(void *) :

    • 线程的入口函数指针。线程创建后,将执行此函数。该函数必须接受一个void *类型的参数,并返回一个void *类型的值。
  • void * restrict arg:

    • 传递给线程入口函数的参数。restrict关键字表示该指针在函数中不会与其他指针指向同一内存区域。

函数调用示例:

#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;
}
  • pthread_create(&t_id, NULL, thread_main, (void*)&thread_param);

    • pthread_t *t_id:线程ID的指针,用来保存创建的线程的标识符。

    • NULL:线程的属性,这里设置为默认属性。

    • thread_main:新线程将执行的函数。

    • (void*)&thread_param:传递给线程函数的参数。

  • 第19行 void* thread_main(void *arg) 中的 arg 参数是第10行传入的 thread_param

运行结果:

image.png

线程相关代码在编译时需要添加 -lpthread 选项,以声明需要连接线程库,只有这样才能调用头文件 pthread.h 中声明的函数。

上述代码使用 sleep 函数让主线程睡眠,以等待子线程完成。这种做法在实际编程中有不妥,可以使用 pthread_join 函数来等待子线程完成:

(2)pthread_join 函数

#include <pthread.h>

// 成功时返回0,失败时返回-1
int pthread_join(pthread_t thread, void **retval);
  • pthread_t thread:

    • 要等待的线程ID。这个参数是之前通过pthread_create创建并返回的线程ID。
  • void **retval:

    • 一个指向指针的指针,用于存储线程退出时的返回值。这个返回值是线程函数返回的void *类型。如果不需要获取线程的返回值,可以传递NULL

函数调用示例:

#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行:针对第13行创建的线程调用 pthread_join 函数。因此 main 函数将会等待 ID 保存在 t_id 变量中的线程终止。
  • 第41行返回的值将保存在第19行第二个参数 thr_ret 中。

上述代码的执行流程:

image.png

(二)线程安全函数与非线程安全函数

(1)临界区的概念

线程的运行需要考虑 “多个线程同时调用函数时(执行时)可能产生问题”。这类函数内部存在临界区(Critical Section),也就是说,多个线程同时执行这部分代码时,可能引起问题。

临界区(Critical Section) 是指多线程或多进程并发程序中,访问共享资源(如全局变量、文件、内存块等)的一段代码。由于多个线程或进程可能同时访问和修改共享资源,从而产生数据竞争和不一致的问题,临界区需要通过适当的同步机制确保在同一时间内只有一个线程或进程可以访问它。

(2)线程安全与非线程安全函数

根据临界区是否引起问题,函数㐓分为下面两类:

  • 线程安全函数(Thread-safe function)
  • 非线程安全函数(Thread-unsafe function)

上述分类并非关于有无临界区的讨论,线程安全的函数中同样可能存在临界区,只不过其内部有一些措施避免函数出现问题。

大多数标准函数都是线程安全的函数。而且我们不用自己区分线程安全的函数和非线程安全的函数,因为平台在定义非线程安全函数的同时,提供了具有相同功能的线程安全的函数。

执行代码时,可以通过添加 -D_REENTRANT 选项定义 _REENTRANT 宏,此宏能自动将非线程安全函数调用改为线程安全函数调用。

线程存在的问题

观察下面的示例:

#include <stdio.h>
#include <windows.h>
#include <process.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[]) 
{
	HANDLE 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 函数。全局变量 num 经过增减过程后应该为0,上述代码运行结果如下:

image.png

可以看到运行结果并不是0,而且每次运行的结果都不同。

(一)多个线程访问同一变量

多个线程访问同一变量会出现问题的原因主要是由于竞态条件(Race Condition)数据一致性问题

(1)竞态条件(Race Condition)

当多个线程同时访问和修改同一变量,并且至少有一个线程对该变量进行了写操作时,程序的行为可能是不确定的。竞态条件发生在以下情况下:

  1. 并发访问:多个线程同时对同一变量进行读写操作。
  2. 操作不原子:对变量的操作是非原子的,即操作可以被中断或打断。例如,num += 1 是由多个步骤组成的操作(读取、计算、写入),这些步骤之间可以被其他线程的操作打断。

(2)数据一致性问题

  1. 缓存一致性:现代多核处理器可能在各个核心中有缓存,每个核心可以缓存共享变量的副本。如果没有适当的同步机制,核心的缓存可能不同步,导致线程看到过时的数据。
  2. 指令重排:编译器或处理器可能会对代码进行优化,改变操作的顺序,这可能会导致在多线程环境下的行为不同于预期。

(二)临界区位置

临界区通常位于由线程运行的函数内部。上述例子中的临界区并非 num 本身,而是访问 num 的2条语句 num += 1;num -= 1;。这两条语句可能由多个线程同时运行,这也是引起问题的直接原因。产生的问题可以整理为如下三种情况:

  • 2个线程同时执行 thread_inc 函数。
  • 2个线程同时执行 thread_des 函数。
  • 2个线程分别执行 thread_inc 函数和 thread_des 函数。

需要关注最后一点,2条不同语句由不同线程同时执行时,也可能构成临界区。 前提是这2条语句访问同一内存空间。

线程同步

前面讨论了线程中存在的问题,接下来就讨论解决方法——线程同步。

(一)同步的两面性

线程同步用于解决线程访问顺序引发的问题。需要同步的情况可以从如下两方面考虑。

  • 同时访问同一内存空间时发生的情况。
  • 需要指定访问同一内存空间的线程执行顺序的情况。

上面已经解释过第一种情况。第二种情况是 “控制线程执行顺序” 的相关内容。假设有 A、B 两个线程,线程 A 负责向指定内存空间写入(保存)数据,线程 B 负责取走该数据。这种情况下,线程 A 应该先访问约定的内存空间并保存数据。万一线程 B 先访问并取走数据,将导致错误结果。

(二)互斥量

(1)概念

互斥量(Mutex, Mutual Exclusion) 是一种用于多线程编程中的同步机制,确保同一时刻只有一个线程可以访问共享资源或临界区,从而避免竞态条件和数据不一致问题。

(2)原理

互斥量通过加锁和解锁机制实现线程同步:

  1. 加锁(Lock) :当一个线程想要访问共享资源时,它首先必须“加锁”互斥量。如果其他线程已经持有该锁,当前线程将会被阻塞,直到锁被释放。
  2. 解锁(Unlock) :当持有锁的线程完成对共享资源的操作后,它必须“解锁”互斥量,允许其他等待的线程获取锁并访问共享资源。

(3)互斥量的常见操作

以下是互斥量的相关操作函数:

  • pthread_mutex_init:初始化互斥量。
  • pthread_mutex_destroy:销毁互斥量。
  • pthread_mutex_lock:加锁,获取互斥量。
  • pthread_mutex_unlock:解锁,释放互斥量。

上述函数的函数原型如下:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • mutex:指向要初始化或销毁的互斥量对象的指针。
  • attr:指向互斥量属性的指针,通常传递 NULL,表示使用默认属性。

创建好互斥量的前提下,可以通过如下结构保护临界区:

pthread_mutex_lock(&mutex);
// 临界区的开始
// ...
// 临界区的结束
pthread_mutex_unlock(&mutex);

简而言之,就是利用 lockunlock 函数围住临界区的两端。需要注意的是,如果忘记调用 pthread_mutex_unlock 函数,那么其他为了进入临界区而调用 pthread_mutex_lock 函数的线程就无法摆脱阻塞状态。这种情况称为 “死锁”(Dead-lock)。

(4)调用示例

下面是互斥量相关函数的调用示例:

#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;
}
  • 注意到 thread_inc 函数和 thread_des 函数临界区的范围不同,其中 thread_inc 将整个循环放在互斥锁的保护之下,这导致加锁时间较长;而 thread_des 将临界区范围缩小到循环里面,导致频繁的加锁和解锁操作,有较大的上下文切换和锁管理开销。然而这里并没有谁好谁不好,需要根据不同程序酌情考虑究竟扩大还是缩小临界区。

(三)信号量

(1)概念

信号量(Semaphore) 是一种用于线程或进程同步的机制,它可以控制多个线程对共享资源的访问,以避免竞态条件。信号量和互斥量的区别在于,互斥量只允许一个线程进入临界区,而信号量允许多个线程同时进入,具体的线程数量由信号量的初始值控制。

(2)原理

  • 信号量的值:信号量有一个内部计数值,它控制可访问共享资源的线程数量。每次有线程访问资源时,信号量的值会减1;每次有线程释放资源时,信号量的值会加1。 如果信号量的值为0,表示没有可用的资源,其他线程会被阻塞,直到信号量值大于0。

  • 两种类型的信号量

    • 计数信号量(Counting Semaphore) :允许指定多个线程进入临界区,适合用于控制多个资源。
    • 二进制信号量(Binary Semaphore) :信号量的值只能是0或1,类似于互斥锁,只允许一个线程进入临界区。

(3)信号量的常见操作

① 初始化信号量

#include <semaphore.h>
// 成功时返回0,失败时返回其他值
int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem:指向要初始化的信号量对象。

  • pshared:为0时,信号量用于线程同步;为非0时,信号量可用于进程间同步。

  • value:信号量的初始值,表示资源的可用数量。

② 销毁信号量

#include <semaphore.h>
// 成功时返回0,失败时返回其他值
int sem_destroy(sem_t *sem);

sem:指向要销毁的信号量对象。

③ 等待信号量

#include <semaphore.h>
// 成功时返回0,失败时返回其他值
int sem_wait(sem_t *sem);

信号量的值大于0时,将信号量值减1,线程可以继续执行;如果信号量的值为0,调用线程将被阻塞,直到其他线程释放信号量。

④ 释放信号量

#include <semaphore.h>
// 成功时返回0,失败时返回其他值
int sem_post(sem_t *sem);

将信号量的值加1,并唤醒一个等待中的线程(如果有)。

(4)调用示例

#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;
}
  • 信号量初始化

    • sem_one:初始化为0,意味着一开始线程 accu 不能运行,直到线程 read 完成第一次输入。
    • sem_two:初始化为1,意味着一开始线程 read 可以执行输入操作。
  • 线程创建

    • 线程 id_t1 运行 read 函数:负责读取输入。
    • 线程 id_t2 运行 accu 函数:负责累加输入的数字。
  • read 函数

    • 这个函数循环执行5次,每次要求用户输入一个整数。
    • 在每次读取用户输入之前,它会调用 sem_wait(&sem_two),确保线程 accu 在上一轮的累加完成之前不会继续输入。
    • 读取输入后,调用 sem_post(&sem_one) 以通知线程 accu 可以进行累加。
  • accu 函数

    • 该函数同样执行5次,每次等待 read 线程输入完成。
    • 使用 sem_wait(&sem_one),直到 read 线程通知它有新的数据(即 sem_post(&sem_one))。
    • 获取数据后,将其累加到 sum 中,然后调用 sem_post(&sem_two),允许 read 线程继续读取下一个数字。
  • 线程同步

    • 两个线程通过信号量进行同步:

      • sem_two 控制 read 线程输入的时机,确保累加线程 accu 完成操作后才继续读取输入。
      • sem_one 控制 accu 线程累加的时机,确保 read 线程提供了新数据之后,累加线程才会累加。

线程的销毁和多线程并发服务器端的实现

(一)线程销毁的方法

Linux 线程并不是在首次调用的线程 main 函数返回时自动销毁,所以用如下2种方法之一加以明确。否则由线程创建的内存空间将一直存在。

pthread_join 函数。

此函数在之前按已经介绍,使用方法不再赘述。调用该函数时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程将进入阻塞状态。

pthread_detach 函数

#include <pthread.h>
// 成功时返回0,失败时返回其他值
int pthread_detach(pthread_t thread);

thread:目标线程的 ID。

调用此函数不会引起线程终止或进入阻塞状态,可以通过该函数引导销毁线程创建的内存空间。调用该函数后不能再针对相应线程调用 pthread_join 函数,这点需要注意。

(二)多线程并发服务器端的实现

下面的代码实现了多个客户端之间可以交换信息的简单的聊天程序。

(1)chat_server.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);
}

上述代码中,访问全局变量 clnt_cnt 和数组 clnt_socks 的代码将构成临界区。、

(2)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);
}

(2)运行结果

image.png