四、多线程服务器

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:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值