TCP网络通信/线程池

来源:互联网 发布:用友u8数据备份 编辑:程序博客网 时间:2024/06/05 16:00

TCP网络通信

预备知识

  1. socket: 在TCP/IP协议中表示:IP地址+TCP端口号或UDP端口号唯一标识网络通讯中的一个进程,IP地址+端口号就称为socket。
  2. 网络字节序: 在学习C语言的时候,我们大家都应该知道大小端 的问题,在网络数据流中同样也有大小端之分,网络数据流的地址规定: 先发出的数据是低地址,后发出的地址是高地址,即网络数据流应采用大端字节序,低地址高字节。
#include<arpa/inet.h>uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohl(uint16_t netshort);

以上4个函数是库中对网络字节序和主机字节序的转换,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

TCP套接字通信

在套接字通信当中,我们分为server端与client端首先从服务器端(server端)讨论:
一、首先生成一个套接字文件,在套接字文件中:

int socket(int domain, int type, int protocol);
  • doamin: 一般我们采用TPv4,所以全部填 AF_INET;

  • type我们正在这介绍两种:

    • SOCK_STREAM(流式服务,在TCP通信中使用)

    • SOCK_DGREAM(数据报服务,在UDP通信中使用)

二、 把服务器端与指定的socket绑定

int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
  • 绑定的结构体是我们自己定义的一个本地的信息;
    三、 把服务器设置为监听状态
int listen(int sockfd, int backlog)

四、服务器开始接受客户端传来的数据

int accept(itn sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 绑定的结构体是我们要接受的客户端的信息,最后一个参数可做输入输出性参数 ;

在服务器端完成以上4个步骤后大致就可以完成服务器端的端口设置;
下面介绍客户端(client)的接口:
一、创建sock文件
二、不需要绑定,也可以绑定;但是一般不建议绑定
三、客户端只需要确保和服务器端连接上就可以了,不需要别的步骤:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 绑定的结构体一定是服务器端的;

上面就是全部接口,下面进行代码的实现:

client端

不管对于是单用户,多进程,多线程中,客户端的代码不会变

#include <stdio.h>#include <sys/socket.h>#include <stdlib.h>#include <sys/types.h>#include <arpa/inet.h>#include <netinet/in.h>#include <unistd.h>#include <string.h>static void usage(const char* proc){    printf("Usage: %s[local_ip] [local_port]\n", proc);}int main(int argc, char *argv[]){    if(argc != 3)    {        usage(argv[0]);        return 1;    }    int sock = socket(AF_INET, SOCK_STREAM, 0);    if(sock < 0)    {        perror("socket");        return 1;    }    struct sockaddr_in server;    server.sin_family = AF_INET;    server.sin_addr.s_addr= inet_addr(argv[1]);    server.sin_port = htons(atoi(argv[2]));    socklen_t len = sizeof(struct sockaddr_in);    if(connect(sock, (struct sockaddr *)&server, len) < 0)    {        perror("connect");        return 2;    }    printf("connect success...\n");    char buf[1024];    while(1)    {        printf("send###");        fflush(stdout);        ssize_t _r = read(0, buf, sizeof(buf)-1);        if(_r > 0)        {            buf[_r] = 0;            write(sock, buf, strlen(buf));        }        write(1, buf, strlen(buf));    }    close(sock);    return 0;}

server端:

单进程

#include <stdio.h>#include <sys/socket.h>#include <stdlib.h>#include <sys/types.h>#include <arpa/inet.h>#include <netinet/in.h>#include <unistd.h>#include <string.h>#include <pthread.h>static void usage(const char* proc){    printf("Usage: %s[local_ip] [local_port]\n", proc);}int startup(const char *_ip, int _port){    int sock = socket(AF_INET, SOCK_STREAM, 0);     //创建套接字文件       if(sock < 0)    {        perror("sock");        exit(2);    }    int opt = 1;    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//解决server bind error的函数    struct sockaddr_in local;    local.sin_family = AF_INET;    local.sin_addr.s_addr = inet_addr(_ip);    local.sin_port = htons(_port);    if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0) //套接字文件与输入IP,port绑定    {        perror("bind");        exit(3);    }    if(listen(sock, 10) < 0)            //将服务器段设置为监听状态    {        perror("listen");        exit(4);    }    return sock;}int main(int argc, char* argv[]){    //./tcp_server ip port    if(argc != 3)    {        usage(argv[0]);        return 1;    }    int listen_sock = startup(argv[1], atoi(argv[2]));    printf("bind and listen success...\n");    while(1)    {        struct sockaddr_in client;        socklen_t len = sizeof(client);        int new_sock = accept(listen_sock, (struct sockaddr *)&client, &len);//创建一个新的将接收的sock分别工作        if(new_sock < 0)        {            perror("accept");            continue;        }        printf("get a client, ip:%s, port:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));//      version 1       单用户, 没什么用了已经,练手来的        while(1)        {            char buf[1024];            ssize_t _r = read(new_sock, buf, sizeof(buf)-1);            if(_r > 0)            {                   buf[_r] = 0;                printf("client#:%s",buf);            }            else if(_r == 0)            {                close(new_sock);                printf("client is quit!\n");                break;            }else            {                perror("read");                close(new_sock);                break;            }        }    }    return 0;}

多进程

#include <stdio.h>#include <sys/socket.h>#include <stdlib.h>#include <sys/types.h>#include <arpa/inet.h>#include <netinet/in.h>#include <unistd.h>#include <string.h>#include <pthread.h>static void usage(const char* proc){    printf("Usage: %s[local_ip] [local_port]\n", proc);}int startup(const char *_ip, int _port){    int sock = socket(AF_INET, SOCK_STREAM, 0);     //创建套接字文件       if(sock < 0)    {        perror("sock");        exit(2);    }    int opt = 1;    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//解决server bind error的函数    struct sockaddr_in local;    local.sin_family = AF_INET;    local.sin_addr.s_addr = inet_addr(_ip);    local.sin_port = htons(_port);    if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0) //套接字文件与输入IP,port绑定    {        perror("bind");        exit(3);    }    if(listen(sock, 10) < 0)            //将服务器段设置为监听状态    {        perror("listen");        exit(4);    }    return sock;}int main(int argc, char* argv[]){    //./tcp_server ip port    if(argc != 3)    {        usage(argv[0]);        return 1;    }    int listen_sock = startup(argv[1], atoi(argv[2]));    printf("bind and listen success...\n");    while(1)    {        struct sockaddr_in client;        socklen_t len = sizeof(client);        int new_sock = accept(listen_sock, (struct sockaddr *)&client, &len);//创建一个新的将接收的sock分别工作        if(new_sock < 0)        {            perror("accept");            continue;        }        printf("get a client, ip:%s, port:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));//      version 2   多进程        pid_t id = fork();        if(id < 0)        {            close(new_sock);        }        if(id == 0)        {//child            close(listen_sock);            if(fork() > 0)            {                exit(0);            }            while(1)            {                char buf[1024];                ssize_t _r = read(new_sock, buf, sizeof(buf)-1);                if(_r > 0)                {                    buf[_r] = 0;                    printf("client#:%s",buf);                }                else if(_r == 0)                {                    close(new_sock);                    printf("client is quit!\n");                    break;                }else                {                    perror("read");                    close(new_sock);                    break;                }            }            close(new_sock);        }        else //father        {            close(new_sock);        }    }    return 0;}

多线程

#include <stdio.h>#include <sys/socket.h>#include <stdlib.h>#include <sys/types.h>#include <arpa/inet.h>#include <netinet/in.h>#include <unistd.h>#include <string.h>#include <pthread.h>static void usage(const char* proc){    printf("Usage: %s[local_ip] [local_port]\n", proc);}int startup(const char *_ip, int _port){    int sock = socket(AF_INET, SOCK_STREAM, 0);     //创建套接字文件       if(sock < 0)    {        perror("sock");        exit(2);    }    int opt = 1;    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//解决server bind error的函数    struct sockaddr_in local;    local.sin_family = AF_INET;    local.sin_addr.s_addr = inet_addr(_ip);    local.sin_port = htons(_port);    if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0) //套接字文件与输入IP,port绑定    {        perror("bind");        exit(3);    }    if(listen(sock, 10) < 0)            //将服务器段设置为监听状态    {        perror("listen");        exit(4);    }    return sock;}//version3 的线程void* handler(void* arg){    int new_sock = (int)arg;    while(1)    {        char buf[1024];        ssize_t _r = read(new_sock, buf, sizeof(buf)-1);        if(_r > 0)        {               buf[_r] = 0;            printf("client#:%s",buf);        }        else if(_r == 0)        {            close(new_sock);            printf("client is quit!\n");            break;        }else        {            perror("read");            close(new_sock);            break;        }    }}int main(int argc, char* argv[]){    //./tcp_server ip port    if(argc != 3)    {        usage(argv[0]);        return 1;    }    int listen_sock = startup(argv[1], atoi(argv[2]));    printf("bind and listen success...\n");    while(1)    {        struct sockaddr_in client;        socklen_t len = sizeof(client);        int new_sock = accept(listen_sock, (struct sockaddr *)&client, &len);//创建一个新的将接收的sock分别工作        if(new_sock < 0)        {            perror("accept");            continue;        }        printf("get a client, ip:%s, port:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));//      version 3   多线程        pthread_t id;        pthread_create(&id, NULL, handler, (void *)new_sock); 主线程向下执行,新线程执行handler        pthread_detach(id);    }    return 0;}

server bind失败的原因

当我们先启动server端,后启动client端,然后Ctrl-C使server端终止,这时马上再运行server,节后就会出现:bind error;这时因为虽然server的应用程序终止了,但是TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口;当我们把client也终止掉以后,client会自动关闭自己的socket描述符,server的TCP连接也收到了client发送的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待2MSL的时间后才能回到CLOSED状态,应为我们先终止的是server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍不能再次监听同样的server端口。
其实,在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为,TCP的连接没有完全断开指的是connfd(127.0.0.1:8080)没有完全断开,而我们重新监听的lesten(0.0.0.0:8080),虽然端口号一样,但是IP不同,connfd对应的食欲某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个sock描述符,在server代码的socket()和bind()调用之间插入

int opt = 1;setsockopt(listenfd, SOL_SOCCKET, SO_REUSEADDR, &opt, sizeof(opt));

线程池

1.线程池基本原理

在传统服务器结构中, 常是 有一个总的 监听线程监听有没有新的用户连接服务器, 每当有一个新的 用户进入, 服务器就开启一个新的线程用户处理这 个用户的数据包。这个线程只服务于这个用户 , 当 用户与服务器端关闭连接以后, 服务器端销毁这个线程。然而频繁地开辟与销毁线程极大地占用了系统的资源。而且在大量用户的情况下, 系统为了开辟和销毁线程将浪费大量的时间和资源。线程池提供了一个解决外部大量用户与服务器有限资源的矛盾, 线程池和传统的一个用户对应一 个线程的处理方法不同, 它的基本思想就是在程序 开始时就在内存中开辟一些线程, 线程的数目是 固定的,他们独自形成一个类, 屏蔽了对外的操作, 而服务器只需要将数据包交给线程池就可以了。当有新的客户请求到达时 , 不是新创建一个线程为其服务 , 而是从“池子”中选择一个空闲的线程为新的客户请求服务 ,服务完毕后 , 线程进入空闲线程池中。如果没有线程空闲 的 话, 就 将 数 据 包 暂 时 积 累 , 等 待 线 程 池 内 有 线 程空闲以后再进行处理。通过对多个任务重用已经存在的线程对象 , 降低了对线程对象创建和销毁的开销。当客户请求 时 , 线程对象 已 经 存 在 , 可 以 提 高 请 求 的响应时间 , 从而整体地提高了系统服务的表现。
2. 线程池的实现

#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/types.h>#include <pthread.h>#include <assert.h>/**线程池里所有运行和等待的任务都是一个CThread_worker*由于所有任务都在链表里,所以是一个链表结构*/typedef struct worker{    /*回调函数,任务运行时会调用此函数,注意也可声明成其它形式*/    void *(*process) (void *arg);    void *arg;/*回调函数的参数*/    struct worker *next;} CThread_worker;/*线程池结构*/typedef struct{    pthread_mutex_t queue_lock;    pthread_cond_t queue_ready;    /*链表结构,线程池中所有等待任务*/    CThread_worker *queue_head;    /*是否销毁线程池*/    int shutdown;    pthread_t *threadid;    /*线程池中允许的活动线程数目*/    int max_thread_num;    /*当前等待队列的任务数目*/    int cur_queue_size;} CThread_pool;int pool_add_worker (void *(*process) (void *arg), void *arg);void *thread_routine (void *arg);static CThread_pool *pool = NULL;void pool_init (int max_thread_num){    pool = (CThread_pool *) malloc (sizeof (CThread_pool));    pthread_mutex_init (&(pool->queue_lock), NULL);    pthread_cond_init (&(pool->queue_ready), NULL);    pool->queue_head = NULL;    pool->max_thread_num = max_thread_num;    pool->cur_queue_size = 0;    pool->shutdown = 0;    pool->threadid =(pthread_t *) malloc (max_thread_num * sizeof (pthread_t));    int i = 0;    for (i = 0; i < max_thread_num; i++)    {         pthread_create (&(pool->threadid[i]), NULL, thread_routine,NULL);    }}/*向线程池中加入任务*/int pool_add_worker (void *(*process) (void *arg), void *arg){    /*构造一个新任务*/    CThread_worker *newworker =        (CThread_worker *) malloc (sizeof (CThread_worker));    newworker->process = process;    newworker->arg = arg;    newworker->next = NULL;/*别忘置空*/    pthread_mutex_lock (&(pool->queue_lock));    /*将任务加入到等待队列中*/    CThread_worker *member = pool->queue_head;    if (member != NULL)    {        while (member->next != NULL)            member = member->next;        member->next = newworker;    }        else    {        pool->queue_head = newworker;    }    assert (pool->queue_head != NULL);    pool->cur_queue_size++;    pthread_mutex_unlock (&(pool->queue_lock));    /*好了,等待队列中有任务了,唤醒一个等待线程;    注意如果所有线程都在忙碌,这句没有任何作用*/    pthread_cond_signal (&(pool->queue_ready));    return 0;}/*销毁线程池,等待队列中的任务不会再被执行,但是正在运行的线程会一直把任务运行完后再退出*/int pool_destroy (){    if (pool->shutdown)        return -1;/*防止两次调用*/    pool->shutdown = 1;    /*唤醒所有等待线程,线程池要销毁了*/    pthread_cond_broadcast (&(pool->queue_ready));    /*阻塞等待线程退出,否则就成僵尸了*/    int i;    for (i = 0; i < pool->max_thread_num; i++)        pthread_join (pool->threadid[i], NULL);    free (pool->threadid);    /*销毁等待队列*/    CThread_worker *head = NULL;    while (pool->queue_head != NULL)    {        head = pool->queue_head;        pool->queue_head = pool->queue_head->next;        free (head);    }    /*条件变量和互斥量也别忘了销毁*/    pthread_mutex_destroy(&(pool->queue_lock));    pthread_cond_destroy(&(pool->queue_ready));    free (pool);    /*销毁后指针置空是个好习惯*/    pool=NULL;    return 0;}void* thread_routine (void *arg){    printf ("starting thread 0x%x\n", pthread_self ());    while (1)    {        pthread_mutex_lock (&(pool->queue_lock));        /*如果等待队列为0并且不销毁线程池,则处于阻塞状态; 注意        pthread_cond_wait是一个原子操作,等待前会解锁,唤醒后会加锁*/        while (pool->cur_queue_size == 0 && !pool->shutdown)        {            printf ("thread 0x%x is waiting\n", pthread_self ());            pthread_cond_wait (&(pool->queue_ready), &(pool->queue_lock));        }        /*线程池要销毁了*/        if (pool->shutdown)        {            /*遇到break,continue,return等跳转语句,千万不要忘记先解锁*/            pthread_mutex_unlock (&(pool->queue_lock));            printf ("thread 0x%x will exit\n", pthread_self ());            pthread_exit (NULL);        }        printf ("thread 0x%x is starting to work\n", pthread_self ());        /*assert是调试的好帮手*/        assert (pool->cur_queue_size != 0);        assert (pool->queue_head != NULL);        /*等待队列长度减去1,并取出链表中的头元素*/        pool->cur_queue_size--;        CThread_worker *worker = pool->queue_head;        pool->queue_head = worker->next;        pthread_mutex_unlock (&(pool->queue_lock));        /*调用回调函数,执行任务*/        (*(worker->process)) (worker->arg);        free (worker);        worker = NULL;    }    /*这一句应该是不可达的*/    pthread_exit (NULL);}void* myprocess (void *arg){    printf ("threadid is 0x%x, working on task %d\n", pthread_self (),*(int *) arg);    sleep (1);/*休息一秒,延长任务的执行时间*/    return NULL;}int main (){    pool_init (3);/*线程池中最多三个活动线程*/    /*连续向池中投入10个任务*/    int *workingnum = (int *) malloc (sizeof (int) * 10);    int i;    for (i = 0; i < 10; i++)    {        workingnum[i] = i;        pool_add_worker (myprocess, &workingnum[i]);    }    /*等待所有任务完成*/    sleep (5);    /*销毁线程池*/    pool_destroy ();    free (workingnum);    return 0;}