四、多线程服务器

news2025/1/10 11:44:39

1.进程的缺陷和线程的优点

1.进程的缺陷

创建进程(复制)的工作本身会给操作系统带来相当沉重的负担。

而且,每个进程具有独立的内存空间,所以进程间通信的实现难度也会随之提高。

同时,上下文切换(Context Switching)过程是创建进程的最大开销。

系统同时运行多个进程,系统将CPU时间分成多个微小的块后分配给了多个进程。为了分时使用CPU,需要上下文切换过程。下面了解一下“上下文切换”的概念。

如果运行进程A后需要紧接着运行进程B,就应该将进程A相关信息移出内存,并读入进程B相关信息。这就是上下文切换。但此时进程A的数据将被移动到硬盘,所以上下文切换需要很长时间。即使通过优化加快速度,也会存在一定的局限。 

2.线程的优点

为了保持多进程的优点,同时在一定程度上克服其缺点,人们引人了线程(Thread )。这是为了将进程的各种劣势降至最低限度(不是直接消除)而设计的一种“轻量级进程”。线程相比于进程具有如下优点。
1.线程的创建和上下文切换比进程的创建和上下文切换更快。

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

2.线程和进程的差异

进程空间:

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

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

1.上下文切换时,不需要切换数据区和堆

2.可以利用数据区和堆交换数据

线程为了保持多条代码执行流而隔开了栈区域,因此拥有下面的内存结构:

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

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

线程:在进程构成单独执行流的单位。 

3.线程创建及运行

POSIX是Portable Operating System Interface for Computer Environment(适用于计算机环境的可移植操作系统接口)的简写,是为了提高UNIX系列操作系统间的移植性而制定的API规范。下面要介绍的线程创建方法也是以POSIX标准为依据的。因此,它不仅适用于Linux,也适用于大部分UNIX系列的操作系统。 

1.线程的创建和执行流程

下面是我在百度找到的关于restrict关键字的描述:

restrict,C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。

1.pthread_create函数

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

(1)thread

保存新创建线程ID的变量地址值。线程与进程相同,也需要用于区分不同线程的ID。

(2)attr

用于传递线程属性的参数,传递NULL时,创建默认属性的线程

(3)start_routine

相当于线程main函数的、在单独执行流中执行的函数地址值(函数指针)

(4)arg

通过第三个参数传递调用函数所包含传递参数信息的变量地址值

代码示例:

#include<stdio.h>
#include<pthread.h>

void* thread_main(void *arg)
{
        int cnt=*((int*)arg);
        int i;
        for(i=0;i<cnt;i++)
        {
                sleep(1);
                puts("running thread");
        }

        return NULL;
}

int main(int argc,char *argv[])
{
        pthread_t trdId;
        int threadParam=5;

        if(pthread_create(&trdId,NULL,thread_main,(void*)&threadParam)!=0)
        {
                puts("pthread_create() error");
                return -1;
        }

        sleep(10);
        puts("end of main");
        return 0;
}

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

虚线代表执行流程,向下的箭头指的是执行流,横向箭头是函数调用。


如果将sleep(10)改成sleep(2),不会像代码中写的那样输出5次“running thread”字符串,如上。因为main函数返回后整个进程将被销毁。如下:

那是否就只要合理使用sleep函数,就能很好的控制线程的执行了呢?

通过调用sleep函数控制线程的执行相当于预测程序的执行流程,但实际上这是不可能完成的。
而且稍有不慎,很可能干扰程序的正常执行流。

因此,为了控制线程的执行流,通常利用下面的函数:

2.pthread_join函数

int pthread_join(pthread_t thread,void** status);
//成功返回0,失败时返回其他值

(1)thread

该参数值ID的线程终止后才会从该函数返回

(2)status

保存线程的main函数返回值的指针变量地址值

也就是说,调用该函数的进程或者线程将进入等待状态,直到ID为第一个参数的线程终止为止。

而且可以得到线程的main函数返回值。代码示例:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
void *thread_main(void *arg)
{
        int cnt=*((int*)arg);
        char *msg=(char*)malloc(sizeof(char)*50);
        strcpy(msg,"Hello, I am thread.\n");

        int i;
        for(i=0;i<cnt;i++)
        {
                sleep(1);
                puts("running thread");
        }
        return (void*)msg;
}

int main(int argc,char *argv[])
{
        pthread_t trd_id;
        int thread_param=5;
        if(pthread_create(&trd_id,NULL,thread_main,(void*)&thread_param)!=0)
        {
                puts("pthread_create() error");
                return -1;
        }

        void* pthread_rtn;
        if(pthread_join(trd_id,&pthread_rtn)!=0)
        {
                puts("pthread_join() error");
                return -1;
        }

        printf("Thread return message: %s\n",(char*)pthread_rtn);
        free(pthread_rtn);

        return 0;
}

其执行流程图如下:

2.临界区

关于线程的运行需要考虑多个线程同时调用函数时可能产生问题。

这类函数内部存在临界区(Critical Section),也就是说多个线程同时执行这部分代码,可能引发问题。

临界区指的是一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问的特性。

当有线程进入临界区段时,其他线程或是进程必须等待,有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用。

因此根据临界区是否引起问题,函数可分为以下2类:

1.线程安全函数(Thread-safe function)

2.非线程安全函数(Thread-unsafe function)

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

但这并非关于有无临界区的讨论,线程安全的函数中同样可能存在临界区。只是在线程安全函数中,同时被多个线程调用时可通过一些措施避免问题。

大多数标准函数都是线程安全的函数。而且不需要自己区分线程安全的函数和非线程安全的函数(在Windows程序中同样如此)。因为这些平台在定义非线程安全函数的同时,提供了具有相同功能的线程安全的函数。如:

提供线程安全的同一函数是:

线程安全函数的名称后缀通常为_r(这与Windows平台不同)。但这种方法会给程序员带来沉重的负担。可以通过声明头文件前定义_REENTRANT宏。自动将gethostbyname函数调用改为gethostbyname_r的函数调用。

gethostbyname函数和gethostbyname_r函数的函数名和参数声明都不同。因此,这种宏声明方式很有用。另外,无需为了上述宏定义特意添加#define语句,可以在编译时通过添加-D REENTRANT选项定义宏。另外,无需为了上述宏定义特意添加#定义语句,可以在编译时通过添加-D_REENTRANT可重入的选项定义宏。如下:


下面编译线程相关代码时均默认添加-D_REENTRANT选项。

3.工作(Worker)线程模型

下面给出创建多个线程的示例:

计算1到10的和,但并不是在main函数中进行累加运算,而是创建2个线程,其中一个线程计算1到5的和,另一个线程计算6到10的和,main函数只负责输出运算结果。

这种方式的编程模型称为工作线程(Worker thread)模型。计算1到5之和的线程与计算6到10之和的线程将成为main线程管理的工作(Worker )(这里是不是应该这样断句,main 线程管理,求大佬告知)。最后,给出示例代码前先给出程序执行流程图:

代码示例:

#include<stdio.h>
#include<pthread.h>

int sum=0;
void *thread_summation(void *arg)
{
        int start=((int*)arg)[0];
        int end=((int*)arg)[1];

        while(start<=end)
        {
                sum+=start;
                ++start;
        }

        return NULL;
}

int main(int argc,char *argv[])
{
        int range1[]={1,5};
        int range2[]={6,10};

        pthread_t trd_id1,trd_id2;
        pthread_create(&trd_id1,NULL,thread_summation,(void*)range1);
        pthread_create(&trd_id2,NULL,thread_summation,(void*)range2);

        void *trd_rtn1,*trd_rtn2;
        pthread_join(trd_id1,&trd_rtn1);
        pthread_join(trd_id2,&trd_rtn2);

        printf("sum=%d\n",sum);
        return 0;
}

结果:

虽然结果正确,但2个线程直接访问了全局变量sum。因此存在着问题,下面举相似的例子证明:

#include<stdlib.h>
#include<unistd.h>

#define NUM_THREAD 100

long long num=0;

void *thread_inc(void *arg)
{
        int i;
        for(i=0;i<50000000;i++)
                num++;
        return NULL;
}
void *thread_des(void *arg)
{
        int i;
        for(i=0;i<50000000;i++)
                num--;
        return NULL;
}

int main(int argc,char *argv[])
{
        pthread_t thread_id[NUM_THREAD];

        printf("sizeof long long: %d\n",sizeof(long long));

        int i;
        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("sum=%lld\n",num);
        return 0;
}

在程序中,我们两个线程main函数分别加减50000000,但最后的结果却不是0。

下面分析错误的原因。

上面的问题出在2个线程正在同时访问全局变量num。此处的访问指的是值的更改。

被访问的是全局变量,但这并非是全局变量引发的问题。实际上任何内存空间,只要被同时访问都可能发生问题。线程虽然是分时使用CPU,但同时访问可能和我们所想的不一样。

最理想的情况:

如下,2个线程要执行将变量值逐次加1的工作

线程1将变量的值增加到100,线程2再访问num时,变量num中将按照我们预想保存101。

注意值的增加方式,值的增加需要CPU运算完成,变量中的值不会自动增加。

线程1首先读该变量的值并将其传递到CPU,获得加1之后的结果为100,最后把结果写回变量num,这样num中就保存100。接下来给出线程2的执行过程:

变量中将保存101,但这是最理想的情况。线程1完全增加num值之前,线程2完全有可能通过切换得到CPU资源。

可能的情况:

线程1读取变量num的值并完成加1运算,但这时加1后的结果尚未写入到变量num。

 在将要把100保存到变量中,但执行该操作前,执行流程转到了线程2。

线程2获取此时num的值仍未99,因为线程1还没有把num加一后的值保存到num,所以线程2重复了线程1的工作,线程2完成后才为100。不控制其他线程访问,实际的情况可能更复杂。

因此,线程访问变量num时应阻止其他线程访问,直到线程1结束。这就是同步(Synchronization)。

因此,为了保证同步,需要划分临界区。

 临界区并非num的定义部分,而是访问num的2条语句。这2条语句可能由多个线程同时使用,也是引起问题的直接原因。产生的问题可以整理为3中情况:

1.2个线程同时执行thread_inc函数

2.2个线程同时执行thread_des函数

3.2个线程分别执行thread_inc函数和thread_des函数

4.线程同步

线程同步可以从下面俩方面考虑:

1.同时访问同一内存空间时发生的情况

2.需要指定访问同一内存空间的线程执行顺序的情况

上面已讨论过第一种情况,因此下面重点讨论第二种。这是控制线程执行顺序的相关内容。

假设有A、B两个线程,线程A负责向指定内存空间写人数据,线程B负责取走该数据。这种情况下,线程A首先应该访问约定的内存空间并保存数据。万一线程B先访问并取走数据,将导致错误结果。像这种需要控制执行顺序的情况也需要使用同步技术。 

下面将会介绍互斥量(Mutex)和信号量(Semaphore)这两种同步技术。

1.互斥量

下面举一个例子:

假如有个电话亭,我们想要使用,则必须保证里面现在没有人在使用。因此,电话亭这个公共的资源不能被多个人占用。则电话亭使用规则如下:

1.为了保护个人隐私,电话亭在有人进来使用时会自动锁上门。

2.如果此时有人在使用电话亭,则需要等待。

3.里面的人使用后,电话亭从里面打开,且电话亭的锁将会打开,直到下一个人的使用。

因此,互斥量的作用就是电话亭的门锁。

1.pthread_mutex_init函数

int pthread_mutex_init(pthread_mutex_t *mutex,
                        const pthread_mutexattr_t *attr);
//成功返回0,失败返回其他值

(1)mutex

创建互斥量时传递保存互斥量的变量地址值

(2)attr

传递即将创建的互斥量属性,没有特别需要指定的属性时传递NULL。

attr可能的值有:

1.PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
2.PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
3.PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样保证当不允许多次加锁时不出现最简单情况下的死锁。
4.PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争

2.pthread_mutex_destory函数

int pthread_mutex_destory(pthread_mutex_t *mutex);
//成功返回0,失败返回其他值

(1)mutex

需要销毁的互斥量地址值

为了创建相当于锁系统的互斥量,需要声明如下pthread_mutex_t变量:

该变量的地址将传递给pthread_mutex_init函数,用来保存操作系统创建的互斥量(锁系统)。

调用pthread_mutex_destory函数时同样需要该信息。如果不需要配置特殊的互斥量属性,则向第二个参数传递NULL时,可以利用PTHREAD_MUTEX_INITIALIZER宏进行如下初始化:

 但通过宏进行初始化很难发现发生的错误,因此上面的做法不推荐,最好用pthread_init函数初始化。

3.pthread_mutex_lock函数

int pthread_mutex_lock(pthread_mutex_t *mutex);
//成功返回0,失败时返回其他值

4.pthread_mutex_unlock函数

int pthread_mutex_unlock(pthread_mutex_t *mutex);
//成功返回0,失败时返回其他值

函数的名字很容易理解。进入临界区前调用的函数就是pthread_mutex_lock函数。相当于进入锁门的过程。因为如果里面有人,则我们将不能进入使用电话亭。所以,调用该函数时,如果发现其他线程已进入临界区,则pthread_mutex_lock函数不会返回,直到里面的线程调用pthread_mutex_unlock退出临界区为止。也就是,其他线程让出临界区之前,当前线程将一直处于阻塞状态。创建好互斥量的前提下,可以通过如下结构保护临界区:

退出临界区时,如果线程忘了调用pthread_mutex_unlock函数,那么其他为了进入临界区而调用pthread_mutex_lock函数的线程就无法摆脱阻塞状态。这种情况称为死锁(Dead-lock)。

#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<unistd.h>

#define NUM_THREAD 100

long long num=0;
pthread_mutex_t mutex;

void *thread_inc(void *arg)
{
        int i;
        pthread_mutex_lock(&mutex);
        for(i=0;i<50000000;i++)
                num++;
        pthread_mutex_unlock(&mutex);
        return NULL;
}
void *thread_des(void *arg)
{
        int i;
        pthread_mutex_lock(&mutex);
        for(i=0;i<50000000;i++)
                num--;
        pthread_mutex_unlock(&mutex);
        return NULL;
}
int main(int argc,char *argv[])
{
        pthread_t thread_id[NUM_THREAD];

        printf("sizeof long long: %d\n",sizeof(long long));

        pthread_mutex_init(&mutex,NULL);

        int i;
        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("sum=%lld\n",num);
        pthread_mutex_destroy(&mutex);
        return 0;
}

结果:

2.信号量

此处只涉及二进制信号量完成控制线程顺序为中心的同步方法。

1.sem_init函数

int sem_init(sem_t *sem,int pshared,unsigned int value);
//成功返回0,失败返回其他值

(1)sem

创建信号量时传递信号量的变量地址值

(2)pshared

传递其他值时,创建可由多个进程共享的信号量;传递0时,创建只允许1个进程内部使用的信号量。我们需要完成同一进程内的线程同步,故传递0。 

(3)value

指定新创建的信号量初始值

2.sem_destroy函数

int sem_destroy(sem_t *sem);
//成功时返回0,失败时返回其他值

(1)sem

传递需要销毁的信号量的变量地址值

3.sem_post函数

int sem_post(sem_t *sem);
//成功返回0,失败返回其他值

(1)sem

传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1。

4.sem_wait函数

int sem_wait(sem_t *sem);
//成功时返回0,失败返回其他值

(1)sem

传递保存信号量读取值的变量地址值,传递给sem_wait时信号量减1。

调用sem_init函数时,操作系统将创建信号量对象,此对象中记录着“信号量值”(Semaphore Value)整数。

该值在调用sem_post函数时增1,调用sem_wait函数时减1。但信号量的值不能小于0,因此,在信号量为0的情况下调用sem_wait函数时,调用函数的线程将进入阻塞状态(因为函数未返回)。当然,此时如果有其他线程调用sem_post函数,信号量的值将变为1,而原本阻塞的线程可以将该信号量重新减为0并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为1)。 

调用sem_wait函数进入临界区的线程在调用sem_post函数前不允许其他线程进入临界区。

信号量的值在0和1之间跳转,因此,具有这种特性的机制称为二进制信号量。 

下面给出一个示例:

线程A从用户输入得到值后存入全局变量num,此时线程B将取走该值并累加。

该过程共进行5次,完成后输出总和并退出程序。

#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>

static sem_t sem_one;
static sem_t sem_two;
static int num;

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;
        int i;
        for(i=0;i<5;i++)
        {
                sem_wait(&sem_one);
                sum+=num;
                sem_post(&sem_two);
        }
        printf("Result: %d \n",sum);
        return NULL;
}

int main(int argc,char *argv[])
{
        sem_init(&sem_one,0,0);
        sem_init(&sem_two,0,1);

        pthread_t id_t1,id_t2;
        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;
}

结果:

4.线程的销毁

销毁线程有以下的三种方法(书上似乎只说了两种方法,第一种不知道算不算,大家帮看看):

1.Linux线程并不是在首次调用的线程main函数返回时自动销毁。

2.pthread_join函数

3.pthread_detach函数

pthread_join函数很好理解,就是等待线程终止,还会引导线程销毁。

1.pthread_detach函数

int pthread_detach(pthread_t pthread);
//成功返回0,失败时返回其他值

(1)thread

终止的同时需要销毁的进程ID

调用该函数不会引起线程终止或进入阻塞状态,可以通过该函数引导销毁线程创建的内存空间。

另外,调用该函数后,不能再针对相应线程调用pthread_join函数。

一个简单的聊天小程序示例:

服务器接收用户发送的信息,然后将其发送给参与聊天的全部用户。客户端发送信息,接收服务端发送的信息。类似一个qq群的作用。

服务器端:

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>

#define BUF_SIZE 100
#define MAX_CLNT 256

int clientCnt=0;
int clientSocks[MAX_CLNT];
pthread_mutex_t mutx;

//send to all
void send_msg(char *msg,int len)
{       
        int i;
        pthread_mutex_lock(&mutx);
        for(i=0;i<clientCnt;i++)
                write(clientSocks[i],msg,len);
        pthread_mutex_unlock(&mutx);
}

void *handle_clnt(void *arg)
{       
        int clientSock=*((int*)arg);
        int readLen=0;
        char msg[BUF_SIZE];
        int i;
        
        while((readLen=read(clientSock,msg,sizeof(msg)))!=0)
                send_msg(msg,readLen);
        
        pthread_mutex_lock(&mutx);
        //remove disconnected client
        for(i=0;i<clientCnt;i++)
        {       
                if(clientCnt==clientSocks[i])
                {
                        while(i++<clientCnt-1)
                        {
                                clientSocks[i]=clientSocks[i+1];
                        }
                        break;
                }
        }

        clientCnt--;
        pthread_mutex_unlock(&mutx);
        close(clientSock);
        return NULL;
}

void printMess(char *mess)
{
        fputs(mess,stderr);
        fputc('\n',stderr);
        exit(1);
}

int main(int argc,char *argv[])
{
        if(argc!=3)
                printMess("argc error");

        pthread_mutex_init(&mutx,NULL);
        int serverSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);

        struct sockaddr_in serverAddr;
        memset(&serverAddr,0,sizeof(serverAddr));
        serverAddr.sin_family=AF_INET;
        serverAddr.sin_port=htons(atoi(argv[1]));
        serverAddr.sin_addr.s_addr=inet_addr(argv[2]);

        if(bind(serverSock,(struct sockaddr*)&serverAddr,sizeof(serverAddr))==-1)
                printMess("bind() error!");

        if(listen(serverSock,5)==-1)
                printMess("listen() error!");

        struct sockaddr_in clientAddr;
        pthread_t id1;
        socklen_t clientAddrLen=sizeof(clientAddr);
        while(1)
        {
                int clientSock=accept(serverSock,(struct sockaddr*)&clientAddr,&clientAddrLen);

                pthread_mutex_lock(&mutx);
                clientSocks[clientCnt++]=clientSock;
                pthread_mutex_unlock(&mutx);

                pthread_create(&id1,NULL,handle_clnt,(void*)&clientSock);
                pthread_detach(id1);
                printf("Connected client IP: %s \n",inet_ntoa(clientAddr.sin_addr));
        }

        close(serverSock);
        return 0;
}

客户端1:

#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>

#define BUF_SIZE 100
#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);
                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 readLen=0;
        int error_rtn=-1;
        while(1)
        {
                readLen=read(sock,name_msg,NAME_SIZE+BUF_SIZE-1);
                if(readLen==-1)
                        return (void*)(-1);
                name_msg[readLen]=0;
                fputs(name_msg,stdout);
        }

        return NULL;
}

void printMess(char *mess)
{
        fputs(mess,stderr);
        fputc('\n',stderr);
        exit(1);
}

int main(int argc,char *argv[])
{
        if(argc!=4)
                printMess("argc error!");

        sprintf(name,"[%s]",argv[1]);
        int clientSock=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);

        struct sockaddr_in serverAddr;
        memset(&serverAddr,0,sizeof(serverAddr));
        serverAddr.sin_family=AF_INET;
        serverAddr.sin_port=htons(atoi(argv[2]));
        serverAddr.sin_addr.s_addr=inet_addr(argv[3]);

        if(connect(clientSock,(struct sockaddr*)&serverAddr,sizeof(serverAddr))==-1)
                printMess("connect() error!");

        pthread_t snd_thread,rcv_thread;
        pthread_create(&snd_thread,NULL,send_msg,(void*)&clientSock);
        pthread_create(&rcv_thread,NULL,recv_msg,(void*)&clientSock);

        pthread_join(snd_thread,NULL);
        pthread_join(rcv_thread,NULL);

        close(clientSock);
        return 0;
}

客户端2:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1100310.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

cmd:读取电脑硬件序列号

一、读取电脑硬件序列号 1.cmd 在没有使用第三方库的情况下&#xff0c;要读取电脑的硬件序列号通常需要使用操作系统提供的工具或命令行。以下是一个示例&#xff0c;展示如何使用Windows操作系统的命令行工具 wmic 来获取硬件序列号&#xff1a; 打开命令提示符&#xff0…

尚硅谷Flink(三)时间、窗口

1 &#x1f3b0;&#x1f3b2;&#x1f579;️ &#x1f3b0;时间、窗口 &#x1f3b2;窗口 &#x1f579;️是啥 Flink 是一种流式计算引擎&#xff0c;主要是来处理无界数据流的&#xff0c;数据源源不断、无穷无尽。想要更加方便高效地处理无界流&#xff0c;一种方式就…

【Linux学习笔记】代码编辑工具vim

1. vim工具基本模式的转换2. vim命令模式下的各种编辑命令2.1. 光标行定位2.2. 光标自由定位2.3. 复制粘贴2.4. 删除2.5. 文本的大小写替换2.6. 文本的替换2.7. 文本的前删后删2.8. 撤销操作 3. vim底行模式下的命令3.1. 设置行号与取消设置行号3.2. 分屏操作3.3. 在不退出vim的…

Postman简单使用

文章目录 一.接口测试流程二、Postman接口测试工具三、接口关联四、全局变量和环境变量 一.接口测试流程 拿到API接口文档&#xff08;从开发拿或者抓包获取&#xff09;&#xff0c;熟悉接口业务&#xff0c;接口地址&#xff0c;错误码等等 编写接口的测试用例以及评审 编写…

SSL证书续费要如何操作

SSL证书一旦到期&#xff0c;网站会立即无法访问&#xff0c;而且会提出不安全警告&#xff0c;如果是电商或者品牌网站影响还是很大的。 SSL证书和域名续费有很大区别&#xff0c;域名续费只要交钱就可以了&#xff0c;SSL证书续费还需要认证和更新服务器SSL证书文件才算收工…

【多线程】JUC(java.util.concurrent)的常见类 信号量 线程安全的集合类

目录 1. Callable接口 1.1 Callable接口和Runnable接口的区别&#xff1f; 1.2 使用Callable接口编写代码。 2. ReentrantLock 可重入锁 3.信号量 semaphore 3.1 Java中信号量的使用 4.CountDownLatch JUC: java.util.concurrent -> 这个包里的内容主要是一些多线程…

智能变电站自动化系统的应用与产品选型

摘要&#xff1a;现如今&#xff0c;智能变电站发展已经成为了电力系统发展过程中的内容&#xff0c;如何提高智能变电站的运行效率也成为电力系统发展的一个重要目标&#xff0c;为了能够更好地促进电力系统安全稳定运行&#xff0c;本文则就智能变电站自动化系统的实现进行了…

青藏高原连续日光诱导叶绿素荧光数据集(2000-2018)

简介&#xff1a; 青藏高原连续日光诱导叶绿素荧光数据集&#xff08;2000-2018&#xff09;是通过MODIS各通道反射率和SIF观测数据建立神经网络模型&#xff0c;从而得到较高时空分辨率的SIF数据&#xff0c;常作为初级生产力的参考。前言 – 人工智能教程 源数据范围为全球&…

网工实验笔记:MQC原理与配置

一、概述 MQC&#xff08;Modular QoS Command-Line Interface&#xff0c;模块化QoS命令行&#xff09;是指通过将具有某类共同特征的数据流划分为一类&#xff0c;并为同一类数据流提供相同的服务&#xff0c;也可以对不同类的数据流提供不同的服务。 MQC三要素 流分类&am…

15-k8s-高级存储之pv与pvc

文章目录 一、相关概念二、创建pv二、创建pvc三、创建pod调用pvc四、StorageClass动态制备pv 一、相关概念 关系 生命周期相关概念 2.1 静态构建&#xff1a;集群管理员创建若干PV卷。这些卷对象带有真实存储的细节信息,并且对集群用户可用(可见)。PV卷对象存在于Kubernetes …

摩尔信使MThings的设备高级参数

摩尔信使MThings支持三级参数管理方案&#xff0c;依次为&#xff1a;数据级、设备级、通道级。 设备级参数不仅包含设备名称、设备地址等常用信息&#xff0c;同时提供了诸多高级参数&#xff0c;其同样是为了满足不同用户应用场景中所面临的差异化需求&#xff0c;以更加灵活…

勒索病毒LockBit2.0 数据库(mysql与sqlsever)解锁恢复思路分享

0.前言 今天公司服务器中招LockBit2.0勒索病毒&#xff0c;损失惨重&#xff0c;全体加班了一天基本解决了部分问题&#xff0c;首先是丢失的文件数据就没法恢复了&#xff0c;这一块没有理睬&#xff0c;主要恢复的是两个数据库&#xff0c;一个是16GB大小的SQLserver数据库&…

安徽阳光心理测量平台目录遍历

安徽阳光心理测量平台目录遍历 FOFA指纹 title"心理测量平台"漏洞复现 路由后拼接/admin/UserFiles/ GET /admin/UserFiles/ HTTP/1.1 Host: {{Hostname}}修复方案 针对路径设定对应权限

注释的重要性与程序员的责任

注释的重要性与程序员的责任 提升代码可读性促进团队协作提高代码可维护性传承知识和经验代码的责任推荐学习 导语&#xff1a;在编写代码的过程中&#xff0c;注释是程序员们经常讨论的话题。有人认为忽视注释等于耍流氓&#xff0c;但也有人觉得注释只是浪费时间。本文将探讨…

软件开发项目文档系列之三如何撰写项目招标文件

前言 招标文件在采购过程中扮演着至关重要的角色&#xff0c;其主要目的是提供清晰而详尽的信息&#xff0c;以确保采购项目的需求得以明确&#xff0c;潜在的投标单位能够清晰理解并遵守相关要求&#xff0c;并最终为采购方提供一个有力的依据来评估和选择最合适的承建单位。…

c++之new和delete

前言 在本文中&#xff0c;您将学习使用new和delete操作在C 中有效地管理内存。 数组可用于存储多个同类型数据&#xff0c;但是使用数组存在严重的缺点。声明数组时应分配内存&#xff0c;但在大多数情况下&#xff0c;直到运行时才能确定所需的确切内存。在这种情况下&#…

python每日一练(8)

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

微信小程序开发指南

前言 微信是一款由中国著名互联网公司腾讯公司开发的社交软件&#xff0c;于2011年1月21日正式上线。在成立后的短短几年时间里&#xff0c;微信以其简单易用的界面和强大的功能&#xff0c;快速赢得了全球用户的青睐。截止2021年&#xff0c;微信已经有超过10亿的活跃用户&am…

如何使用 OpenSSL 来检查证书,来确保网络通信的安全性?

OpenSSL 是一个强大的安全套接字层密码库&#xff0c;包含丰富的加密算法、常用的密钥和证书封装管理功能以及 SSL/TLS 协议&#xff0c;并提供了丰富的应用程序供测试或其他目的使用。要使用 OpenSSL 来检查证书以确保网络通信的安全性&#xff0c;您可以遵循以下步骤&#xf…

【ARM Coresight Debug 系列 16 -- Linux 断点 BRK 中断使用详细介绍】

文章目录 1.1 ARM BRK 指令1.2 BRK 立即数宏定义介绍1.3 断点异常处理流程1.3.1 el1_sync_handler1.3.2 el1_dbg 跟踪 1.4 debug 异常处理函数注册1.4.1 brk 处理函数的注册 1.1 ARM BRK 指令 ARMv8 架构的 BRK 指令是用于生成一个软件断点的。当处理器执行到 BRK 指令时&…