Linux网络编程(十六)——多线程服务器端的实现

文章目录

[16 多线程服务器端的实现](#16 多线程服务器端的实现)

[16.1 理解线程的概念](#16.1 理解线程的概念)

[16.1.1 引入线程的背景](#16.1.1 引入线程的背景)

[16.1.2 线程和进程的差异](#16.1.2 线程和进程的差异)

[16.2 线程创建及运行](#16.2 线程创建及运行)

[16.2.1 线程的创建和执行流程](#16.2.1 线程的创建和执行流程)

[16.2.2 可在临界区内调用的函数](#16.2.2 可在临界区内调用的函数)

[16.2.3 工作(Worker)线程模型](#16.2.3 工作(Worker)线程模型)

[16.3 线程存在的问题和临界区](#16.3 线程存在的问题和临界区)

[16.3.1 多个线程同时访问同一变量的问题](#16.3.1 多个线程同时访问同一变量的问题)

[16.3.2 临界区位置](#16.3.2 临界区位置)

[16.4 线程同步](#16.4 线程同步)

[16.4.1 同步的两面性](#16.4.1 同步的两面性)

[16.4.2 互斥量(互斥锁)](#16.4.2 互斥量(互斥锁))

[16.4.3 信号量](#16.4.3 信号量)

[16.5 线程的销毁和多线程并发服务器端的实现](#16.5 线程的销毁和多线程并发服务器端的实现)

[16.5.1 销毁线程的3种方法](#16.5.1 销毁线程的3种方法)

[16.5.2 多线程并发服务器端的实现](#16.5.2 多线程并发服务器端的实现)


16 多线程服务器端的实现

16.1 理解线程的概念

16.1.1 引入线程的背景

多进程模型与 select 或 epoll 相比的确有自身的优势,但同时也有问题,如前所述,创建进程(复制)的工作本身会给操作系统带来相当沉重的负担。而且,每个进程具有独立的内存空间,所以进程间通信的实现难度也会随之提高。多进程模型的缺点可概括如下。

  • 创建进程的过程会带来一定的开销。
  • 为了完成进程间数据交换,需要特殊的IPC技术。
  • 频繁的上下文切换。(开销最大)

为了保证多进程的优点,同时在一定程度上克服其缺点,人们引人了线程(Thread)。这是为了将进程的各种劣势降至最低限度而设计的一种 "轻量级进程"。线程相比于进程具有如下优点。

  • 线程的创建和上下文切换比进程的创建和上下文切换更快。
  • 线程间交换数据时无需特殊技术。

16.1.2 线程和进程的差异

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

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

  • 上下文切换时不需要切换数据区和堆。
  • 可以利用数据区和堆交换数据。

实际上这就是线程。线程为了保持多条代码执行流而隔开了栈区域。因此具有下图的内存结构

多个线程将共享数据区和堆。为了保持这种结构,线程将在进程内创建并运行。也就是说,进程和线程可以定义为如下形式。

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

16.2 线程创建及运行

16.2.1 线程的创建和执行流程

(1)线程创建 pthread_create

cpp 复制代码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                        void* (*start_routine)(void *), void *arg);
/**
* thread: 指向线程标识符的指针,线程创建成功时,用于存储新创建线程的线程标识符(线程号)
* attr: 用来设置线程的属性,如优先级等。通常传入默认属性NULL
* void *(*start_routine)(void *): 一个指向函数的指针,即线程接下来要执行的函数。
*                     这个函数必须接收一个void* 类型的参数,并返回 void* 类型的结果
* arg: start_routine 函数的参数,也可以是一个指向任意或空类型数据的指针
* return: 成功时返回0,失败返回非0值
*/

下面通过示例了解该函数的使用

cpp 复制代码
void* thread_main(void* arg) {
    //因为传进来的参数是int类型,(int*)表示将void*强转成int*,然后通过"*"取地址里的值
    int cnt = *(int*)arg;
    for(int i = 0;i < cnt;i++) {
        sleep(1);
        puts("running thread");
    }
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t t_id;
    int thread_param = 5;
    //将thread_param作为thread_main函数的参数传入
    if(pthread_create(&t_id,NULL,thread_main,&thread_param)!=0) {
        puts("pthread_create() error");
        return -1;
    }
    sleep(10);  //让主线程睡眠以等待子线程执行完成
    puts("end of main");
    return 0;
}

上述示例中通过调用 sleep 函数向线程提供了充足的执行时间。但实际上通过调用sleep函数控制线程的执行相当于预测程序的执行流程,但实际上这是不可能完成的事情。而且稍有不慎,很可能干扰程序的正常执行流。例如,怎么可能在上述示例中准确预测 thread_main 函数的运行时间,并让 main 函数恰好等待这么长时间呢?因此,我们不用sleep函数,而是通常利用下面的函数控制线程的执行流。

(2)线程终止 pthread_join

cpp 复制代码
int pthread_join(pthread_t thread, void **retval);
/**
* 等待指定线程结束,获取目标线程的结果返回值,并在目标线程结束后回收它的资源
*
* thread: 指定线程 ID,该线程终止后才会从函数返回
* void **retval: 可选参数,用于接收线程结束后传递的返回值。
*         如果非空,pthread_join在成功时将线程退出状态复制到*retval所指向的内存位置。
*         如果线程没有显式地通过 pthread_exit 提供返回值,则该参数将被设为 NULL 或忽略
* return: 成功时返回0,失败时返回非0
*/

简而言之,调用该函数的进程(线程)将进入等待状态,直到第一个参数为ID的线程终止为止。下面通过实例来了解该函数的功能。

cpp 复制代码
void* thread_main(void* arg) {
    int cnt = *(int*)arg;
    //为了在主函数中获取返回值,应避免将数据创建在栈区,因为函数返回时,栈区数据会超出作用域
    //可以选择在堆区或使用全局变量来存储返回值。
    char* msg = malloc(sizeof(char)*50);
    strcpy(msg,"Hello,I'am thread~\n");
    for(int i = 0;i < cnt;i++) {
        sleep(1);
        puts("running thread");
    }
    return (void*)msg;  //返回堆区数据
}

int main(int argc, char const *argv[])
{
    pthread_t t_id;
    void* thr_ret;    //因为不知道返回值会是什么类型,因此定义void*
    int thread_param = 5;
    if(pthread_create(&t_id,NULL,thread_main,&thread_param)!=0) {
        puts("pthread_create() error");
        return -1;
    }
    //thr_ret保存了线程完成时的状态(堆区里的值)
    if(pthread_join(t_id,(void**)&thr_ret)!=0) {
        puts("pthread_join() error");
        return -1;
    }
    printf("Thread return message: %s \n",(char*)thr_ret);
    free(thr_ret);      //堆区数据要手动释放
    return 0;
}

最后,为了更好地理解示例,给出其执行流程图

16.2.2 可在临界区内调用的函数

上述的示例中,我们只是创建了一个线程。无论创建多少线程,其创建方法没有什么区别。但线程的运行需要考虑 "多个线程同时调用函数时可能产生的问题"。这类函数内部存在临界区,也就是说,多个线程同时执行这部分代码时可能引起问题。根据临界区是否引起问题,函数可分为2类。

  • 线程安全函数。线程安全函数被多个线程同时调用时也不会引发问题。
  • 非线程安全函数。非线程安全函数被同时调用时会引发问题

幸运的是,大多数标准函数都是线程安全的函数。更幸运的是,我们不用自己区分线程安全的函数和非线程安全的函数。因为这些平台在定义非线程安全函数的同时,提供了具有相同功能的线程安全的函数。例如 gethostbyname 就不是线程安全函数,但是操作系统也同时提供线程安全的同一功能函数 gethostbyname_r。

线程安全函数的名称后缀通常为 r。当然!但这种方法会给程序员带来"沉重"的负担。幸好可以通过如下方法自动将 gethostbyname 函数调用改为 gethostbyname_r 函数调用!

"声明头文件前定义_RBENTRANT 宏"

gethostbyname 函数和 gethostbyname_r 函数的函数名和参数声明都不同,因此,这种宏声明方式拥有巨大的吸引力。另外,无需为了上述宏定义特意添加 #define 语句,可以在编译时通过添加 -D_REENTRANT 选项定义宏。

cpp 复制代码
gcc -D_REENTRANT mythread.c -o mthread

16.2.3 工作(Worker)线程模型

接下来介绍的示例将计算1到10的和,但并不是在main函数中进行累加运算,而是创建2个线程,

其中一个线程计算1到5的和,另一个线程计算6到10的和,main函数只负责输出运算结果。这种

方式的编程模型称为 "工作线程(Workerthread)模型"。计算1到5之和的线程与计算6到10之和

的线程将成为main线程管理的工作(Worker)。

cpp 复制代码
void* thread_summation(void* arg) {
    int start = ((int*)arg)[0];
    int end = ((int*)arg)[1];
    int* sum = malloc(sizeof(int));
    *sum = 0;
    for(int i = start; i <= end; i++) {
        *sum += i;
    }
    return (void*)sum;
}

int main(int argc, char const *argv[])
{
    void* result1;
    void* result2;
    pthread_t t_id1,t_id2;
    int range1[] = {1,5}, range2[] = {6,10};
    pthread_create(&t_id1,NULL,thread_summation,range1);
    pthread_create(&t_id2,NULL,thread_summation,range2);
    pthread_join(t_id1,(void**)&result1);
    pthread_join(t_id2,(void**)&result2);
    printf("result is %d\n",*(int*)result1 + *(int*)result2);
    free(result1);free(result2);
    return 0;
}

16.3 线程存在的问题和临界区

我们在上一个案例基础上进行拓展,我们通过 20000 个线程对 num 的值进行自增,代码如下

cpp 复制代码
#define THREAD_COUNT 20000
void *add_thread(void *arg) {
    int *p = (int *)arg;
    (*p)++;
    return (void *)0;
}

int main(int argc, char const *argv[])
{
    pthread_t pid[THREAD_COUNT];
    int num = 0;
    for(size_t i = 0;i < THREAD_COUNT;i++) {
        pthread_create(pid + i,NULL,add_thread,&num);
    }
    for(size_t i = 0;i < THREAD_COUNT;i++) {
        pthread_join(pid[i],NULL);
    }
    printf("累加的结果是%d\n",num);
    return 0;
}

上述图可以看出,每次运行的结果均不同!这就是我们接下来要试着分析的问题。

16.3.1 多个线程同时访问同一变量的问题

在详细解释问题之前,先了解一个概念---------寄存器。这个寄存器就和它的名字一样,是存放值的地方。当线程从进程中获取值进行运算的时候,线程实际上会将这个值放入自己的寄存器,在计算完成后才会重新更新进程内部的数据区。这种机制看似没啥问题,但是,如果一个线程在将寄存器中的值更新回数据区之前它被操作系统中断掉,转而运行第二个线程,那么此时第二个线程实际上使用的是一个过期的值!这个过程发生几次最后的误差就会有多大。所以这个问题就是:"两个线程正在同时访问全局变量num"

16.3.2 临界区位置

临界区定义为如下形式:"函数内同时运行多个线程时引起问题的多条构成的代码块。"那么上述的伪代码描述中,对 num 自增语句是临界区。

16.4 线程同步

16.4.1 同步的两面性

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

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

之前已解释过前一种情况,因此重点讨论第二种情况。这是 "控制线程执行顺序" 的相关内容。假设有A、B两个线程,线程A负责向指定内存空间写入(保存)数据,线程B负责取走该数据。这种情况下,线程A首先应该访问约定的内存空间并保存数据。万一线程B先访向并取走数据,将导致错误结果。像这种需要控制执行顺序的情况也需要使用同步技术。

为了解决上诉线程之间的竞争条件问题,我们可以采用两种方法避免竞争条件

  • 避免多线程写入一个地址

  • 给资源加锁,使同一时间操作临界资源的线程只有一个。

16.4.2 互斥量(互斥锁)

(1)pthread_mutex_t

pthread_mutex_t 是一个定义在头文件 <pthreadtypes.h> 中的联合体类型的别名

cpp 复制代码
typedef union { 
    struct __pthread_mutex_s __data; 
    char __size[__SIZEOF_PTHREAD_MUTEX_T];
    long int __align; 
} pthread_mutex_t; 

pthread_mutex_t 用作线程之间的互斥锁,互斥锁是一种同步机制,用来控制对共享资源的访问。在任何时刻,最多只能有一个线程持有特定的互斥锁。如果一个线程试图获取一个已经被其他线程持有的锁,那么请求锁的线程将被阻塞,直到锁被释放。其基本操作如下:

  • 初始化(pthread_mutex_init):创建互斥锁并初始化,分为静态初始化和动态初始化

  • 锁定 (pthread_mutex_lock):获取互斥锁。如果锁已经被其他线程持有,调用线程将阻塞。

  • 尝试锁定 (pthread_mutex_trylock):非阻塞获取互斥锁。如果锁已被持有,立即返回而不是阻塞。

  • 解锁 (pthread_mutex_unlock):释放互斥锁,使其可被其他线程获取。

  • 销毁 (pthread_mutex_destroy):清理互斥锁资源。

(2)动态初始化互斥锁 pthread_mutex_init

cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t* attr);
/*
* 创建互斥锁并初始化
* mutex:创建互斥量时传递互斥量的变量地址
* attr:传递即将创建的互斥量属性
* return:成功时返回0,失败时返回其他值
*/

(3)静态初始化互斥锁

PTHREAD_MUTEX_INITIALIZER 是 POSIX 线程(Pthreads)库中定义的一个宏,用于静态初始化互斥锁(mutex),我们只需要执行这个宏定义就可以初始化互斥锁了,静态初始化锁不需要我们显示摧毁 (推荐各位尽可能使用 pthread_mutex_init 函数进行初始化,因为通过宏进行初始化时很难发现发生的错误。)

cpp 复制代码
static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

(4)显式销毁互斥锁 pthread_mutex_destroy

cpp 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/*
* 清理互斥锁资源  动态初始化互斥锁时才需要显式进行销毁
* mutex:创建互斥量时传递互斥量的变量地址
* return:成功时返回0,失败时返回其他值
*/

(5)获取互斥锁 pthread_mutex_lock()

cpp 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);
/**
* 获取锁,如果此时锁被占则阻塞
* mutex:锁
* return:获取锁结果 成功时返回0 失败时返回错误码
*/

(6)释放互斥锁 pthread_mutex_unlock()

cpp 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/**
* mutex:锁
* return:释放锁结果 成功时返回0 失败时返回错误码
*/

(7)尝试获取互斥锁 pthread_mutex_trylock()

cpp 复制代码
int pthread_mutex_trylock(pthread_mutex_t *mutex);
/**
* 非阻塞式获取锁,如果锁此时被占则返回 EBUSY
* pthread_mutex_t *mutex:锁
*  return:获取锁结果 如果成功锁定互斥锁,则返回 0;
*          如果互斥锁已被其他线程锁定,返回 EBUSY;
*          其他错误返回不同的错误码。
*/

(8)测试示例

cpp 复制代码
#define THREAD_COUNT 20000
//方法一:静态初始化锁
// static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
//方法二:动态初始化互斥锁
pthread_mutex_t counter_mutex;

void *add_thread(void *arg) {
    int *p = (int *)arg;
    //累加之前  获取锁  保证只有一个线程对其累加
    pthread_mutex_lock(&counter_mutex);
    (*p)++;
    //累加之后  释放锁  
    pthread_mutex_unlock(&counter_mutex);
    return (void *)0;
}

int main(int argc, char const *argv[])
{
    pthread_t pid[THREAD_COUNT];
    //初始化互斥锁
    pthread_mutex_init(&counter_mutex,NULL);
    int num = 0;
    for(size_t i = 0;i < THREAD_COUNT;i++) {
        pthread_create(pid + i,NULL,add_thread,&num);
    }
    for(size_t i = 0;i < THREAD_COUNT;i++) {
        pthread_join(pid[i],NULL);
    }
    printf("累加的结果是%d\n",num);
    //如果是动态初始化互斥锁,必须显示销毁
    pthread_mutex_destroy(&counter_mutex);
    return 0;
}

注意: 对于静态初始化的互斥锁,我们可以不必显示销毁,因为在进程结束时,操作系统会回收该进程的所有资,包括内存、打开的文件描述符和互斥锁等。但是某些情况下,需要对互斥锁显示销毁,如果互斥锁是动态分配的(使用 pthread_mutex_init 函数初始化),或者互斥锁会被跨多个函数或文件使用,不再需要时必须显式销毁。

16.4.3 信号量

信号量与互斥量极为相似,在互斥量的基础上很容易理解信号量。此处只涉及利用 "二进制信号量"(只用0和1)完成 "控制线程顺序" 为中心的同步方法。

信号量的两个操作:P操作和V操作

  • P 操作(Proberen,尝试):也称为等待操作(wait),用于减少信号量的值。如果信号量的值大于 0,它就减1并继续执行;如果信号量的值为0,则进程或线程阻塞直到信号量的值变为非零。

  • V 操作(Verhogen,增加):也称为信号操作(signal),用于增加信号量的值。如果有其他进程或线程因信号量的值为 0 而阻塞,这个操作可能会唤醒它们。

(1)初始化信号量 sem_init

cpp 复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);
/**
* 在sem指向的地址初始化一个无名信号量
* sem:指向信号量的指针
* pshared:(1)如果为0,信号量只在线程间共享,应该被置于所有线程均可见地址
*             (全局变量或在堆中动态分配的变量);
*          (2)如果非零,信号量可以在进程间共享,应该被置于共享内存区域,
*              任何进程只要访问共享内存区域,即可操作进程间共享的信号量
* value:信号量的初始值
* return:成功返回 0,失败返回-1,同时errno被设置以记录错误信息
*/

(2)摧毁信号量 sem_destroy

cpp 复制代码
int sem_destroy(sem_t * sem)
/**
* 释放信号量自己占用的一切资源
* sem:指向信号量的指针
* return:成功返回 0,失败返回-1,同时errno被设置以记录错误信息
*/

(3)P操作 sem_wait

cpp 复制代码
int sem_wait(sem_t * sem)
/**
* 将sem指向的信号量减一,如果信号量的值大于0,函数可以执行减一操作,然后立即返回,
* 调用线程继续执行。如果当前信号量是0,则调用阻塞直至信号量值大于0,或信号处理函数打断当前调用
* 
* sem:指向信号量的指针
* return:成功返回 0,失败返回-1,同时errno被设置以记录错误信息
*/

(4)V操作 sem_post

cpp 复制代码
int sem_post(sem_t * sem)
/**
* 将sem指向的信号量加一,如果信号量从0变为1,且其他进程或线程因信号量而阻塞,
* 则阻塞的进程或线程会被唤醒并获取信号量,然后继续执行
* 
* sem:指向信号量的指针
* return:成功返回 0,失败返回-1,同时errno被设置以记录错误信息
*/

(5)获取UNIX时间戳 time

cpp 复制代码
time_t time(time_t *tloc)
/**
* 返回以秒为单位的UNIX时间戳
* tloc:记录时间的指针,如果不为NULL,则当前UNIX秒级时间戳也会存在tloc指向的位置,否则不会存储
* return:成功返回以秒为单位的UNIX时间戳,失败返回-1
*/

(6)测试示例

该示例的场景如下:"线程A从用户输入得到值后存入全局变量num,此时线程B将取走该值并累加。该过程共进行5次,完成后输出总和并退出程序。"

cpp 复制代码
int num;
sem_t sem_input;    //表示输入一个新数据
sem_t sem_output;   //表示拿走一个数据
void* read(void* arg) {
    for(int i = 0;i < 5;i++) {
        printf("第%d次输入数据:",i+1);
        //P操作
        sem_wait(&sem_input);
        scanf("%d",&num);
        //V操作
        sem_post(&sem_output);
    }
    return NULL;
}

void* accu(void* arg) {
    int sum = 0;
    for(int i = 0;i < 5;i++) {
        sem_wait(&sem_output);
        sum += num;
        sem_post(&sem_input);
    }
    printf("结果是:%d\n",sum);
    return NULL;
}

int main(int argc, char const *argv[])
{
    pthread_t id_t1,id_t2;
    //初始时可以输入一个值到num,因此信号量sem_input初值为1
    sem_init(&sem_input,0,1);    
    //初始时不能输出num的值(现在num还没被赋值),因此信号量sem_output初值为0
    sem_init(&sem_output,0,0);   
    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_input);
    sem_destroy(&sem_output);
    return 0;
}

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

16.5.1 销毁线程的3种方法

Linux 线程并不是在首次调用的线程 main 函数返回时自动销毁,所以用如下2种方法之一加以

明确。否则由线程创建的内存空间将一直存在。

  • 调用 pthread_join 函数。
  • 调用 pthread_detach 函数。
cpp 复制代码
int pthread_detach(pthread_t thread);
/**
* 将线程标记为 detached 状态。POSIX 线程终止后,默认情况下创建线程后,它处于可 join 状态,
* 此时可以调用 pthread_join 等待线程终止并回收资源。但是如果主线程不需要等待子线程终止,
* 可以将其标记为 detached状态
*
* pthread_t thread:终止的同时需要销毁的线程 ID
* return:成功返回 0,失败返回错误码
*/

16.5.2 多线程并发服务器端的实现

本节并不打算介绍回声服务器端,而是介绍多个客户端之间可以交换信息的简单的聊天程序。无论服务器端还是客户端,代码量都不少,故省略可以从其他示例中得到或从源代码中复制的头文件声明。同时最大程度地减少异常处理的代码。

(1)服务器端 chat_server.c

cpp 复制代码
#define BUF_SIZE 1024
#define MAX_CLINT 256    //限制最大用户数
int clnt_socks[MAX_CLINT];
pthread_mutex_t mutx;
int clnt_cnt = 0;

void send_msg(char* msg,int len) {
    //广播发送给所有客户端。因为广播发送的时候,可能会有用户加入或断开,
    //clnt_cnt的值可能会发生变化,因此需要加锁
    pthread_mutex_lock(&mutx);
    for(int i = 0;i < clnt_cnt;i++)
        send(clnt_socks[i],msg,len,0);
    pthread_mutex_unlock(&mutx);
}

void* read_from_client_then_write(void *arg) {
    int count, clnt_sock = *(int *)arg;
    char *message = malloc(sizeof(char)*BUF_SIZE);
    if (!message) {
        printf("初始化缓冲失败");
        close(clnt_sock);
        perror("message");
        return NULL;
    }
    while(count = recv(clnt_sock,message,BUF_SIZE,0)) {
        if(count < 0) perror("recv");
        send_msg(message,count);
    }
    //断开连接 移除断连的客户端
    pthread_mutex_lock(&mutx);
    for(int 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);
    free(message);
}

int main(int argc, char const *argv[])
{
    pthread_t pid_read_write;
    struct sockaddr_in serv_addr,clnt_addr;
    memset(&serv_addr,0,sizeof(serv_addr));
    memset(&clnt_addr,0,sizeof(clnt_addr));
    if(argc!=2) {
        printf("Usage:%s <port>\n",argv[0]);
        exit(1);
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[1]));
    inet_pton(AF_INET,"0.0.0.0",&serv_addr.sin_addr);
    int serv_sock = socket(AF_INET,SOCK_STREAM,0);
    int temp=bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
    temp = listen(serv_sock,128);

    while(1) {
        socklen_t clnt_len = sizeof(clnt_addr);
        int clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_len);
        pthread_mutex_lock(&mutx);
        clnt_socks[clnt_cnt] = clnt_sock;
        clnt_cnt++;
        pthread_mutex_unlock(&mutx);
        printf("与客户端%s %d建立连接 文件描述符是%d\n",
            inet_ntoa(clnt_addr.sin_addr),ntohs(clnt_addr.sin_port),clnt_sock);
        //创建一个线程去处理连接的客户端的数据
        if(pthread_create(&pid_read_write,NULL,
            read_from_client_then_write,(void*)&clnt_sock)) 
        {
            perror("pthread_create");
        }
        //需要等待线程结束 但是不能挂起等待
        pthread_detach(pid_read_write);
    }
    close(serv_sock);
    return 0;
}

(2)客户端 chat_clint.c

cpp 复制代码
#define BUF_SIZE 1024
#define NAME_SIZE 20
char name[NAME_SIZE]="[DEFAULT]";
char msg[BUF_SIZE];

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);
        send(sock, name_msg, strlen(name_msg),0);
    }
    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=recv(sock, name_msg, NAME_SIZE+BUF_SIZE-1,0);
       if(str_len==-1)return (void*)-1;
       name_msg[str_len]=0;
       fputs(name_msg, stdout);
     }
     return NULL;
}

int main(int argc, char const *argv[])
{
    void* thread_return;
    pthread_t send_thread,rcv_thread;
    struct sockaddr_in serv_addr;
    memset(&serv_addr,0,sizeof(serv_addr));
    if(argc!=4) {
        printf("Usage : %s <IP> <port> <name>\n",argv[0]);
        exit(1);
    }
    sprintf(name,"[%s]",argv[3]);
    serv_addr.sin_family = AF_INET;
    inet_pton(AF_INET,argv[1],&serv_addr.sin_addr);
    serv_addr.sin_port = htons(atoi(argv[2]));
    int sock = socket(PF_INET,SOCK_STREAM, 0);
    int temp = connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
    pthread_create(&send_thread,NULL,send_msg,(void*)&sock);
    pthread_create(&rcv_thread,NULL,recv_msg,(void*)&sock);
    pthread_join(send_thread,&thread_return);
    pthread_join(rcv_thread,&thread_return);
    close(sock);
    return 0;
}
相关推荐
洁✘18 分钟前
shell编程正则表达式与文本处理器
linux·运维·正则表达式
小白iP代理31 分钟前
随机IP的重要性:解锁网络世界的无限可能
网络·网络协议·tcp/ip·网络安全
深夜面包36 分钟前
Ubuntu 安装与配置 Docker
linux·ubuntu·docker
猫猫与橙子39 分钟前
ubuntu22.04安装dukto
linux·运维·服务器
2302_799525741 小时前
【Linux】su、su-、sudo、sudo -i、sudo su - 命令有什么区别?分别适用什么场景?
linux·运维·服务器
暴躁的小胡!!!1 小时前
2025年最新总结安全基础(面试题)
网络·安全·web安全
正点原子1 小时前
【正点原子STM32MP257连载】第四章 ATK-DLMP257B功能测试——EEPROM、SPI FLASH测试 #AT24C64 #W25Q128
linux·stm32·单片机·嵌入式硬件·stm32mp257
christine-rr2 小时前
【25软考网工笔记】第二章 数据通信基础(4)数据编码
网络·笔记·信息与通信·软考·考试
野生派蒙2 小时前
Linux:安装 CentOS 7(完整教程)
linux·运维·服务器·centos
胡狼FPGA2 小时前
手撕网络协议,实现100G网络UDP通信
网络·网络协议·udp