美文网首页测试C++实现的
六、TCP详解和并发服务器

六、TCP详解和并发服务器

作者: 木鱼_cc | 来源:发表于2018-07-24 15:15 被阅读271次

    1.TCP

    下图是基于TCP协议的客户端/服务器程序的一般流程:


    TCP协议通讯流程

    服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

    数据传输的过程:
    建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发
    回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

    如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,
    服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

    在学习socket API时要注意应用程序和TCP协议层是如何交互的:

    • 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
    • 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,
    • 再比如read()返回0就表明收到了FIN段

    2.出错处理封装函数

    第五篇最后的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。

    为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块wrap.c:

    #include <stdlib.h>
    #include <errno.h>
    #include <sys/socket.h>
    
    //在多线程的时候注意不要用到exit函数,因为它是终止进程的
    void perr_exit(const char *s)
    {
        perror(s);
        exit(1);
    }
    
    int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
    {
        int n;
    
    again:
        if ( (n = accept(fd, sa, salenptr)) < 0) {
            if ((errno == ECONNABORTED) || (errno == EINTR))
                goto again;
            else
                perr_exit("accept error");
        }
        return n;
    }
    
    void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
    {
        if (bind(fd, sa, salen) < 0)
            perr_exit("bind error");
    }
    
    void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
    {
        if (connect(fd, sa, salen) < 0)
            perr_exit("connect error");
    }
    
    void Listen(int fd, int backlog)
    {
        if (listen(fd, backlog) < 0)
            perr_exit("listen error");
    }
    
    int Socket(int family, int type, int protocol)
    {
        int n;
    
        if ( (n = socket(family, type, protocol)) < 0)
            perr_exit("socket error");
        return n;
    }
    
    ssize_t Read(int fd, void *ptr, size_t nbytes)
    {
        ssize_t n;
    
    again:
        if ( (n = read(fd, ptr, nbytes)) == -1) {
            if (errno == EINTR)
                goto again;
            else
                return -1;
        }
        return n;
    }
    
    ssize_t Write(int fd, const void *ptr, size_t nbytes)
    {
        ssize_t n;
    
    again:
        if ( (n = write(fd, ptr, nbytes)) == -1) {
            if (errno == EINTR)
                goto again;
            else
                return -1;
        }
        return n;
    }
    
    void Close(int fd)
    {
        if (close(fd) == -1)
            perr_exit("close error");
    }
    ssize_t Readn(int fd, void *vptr, size_t n)
    {
        size_t  nleft;
        ssize_t nread;
        char   *ptr;
    
        ptr = vptr;
        nleft = n;
        while (nleft > 0) {
            if ( (nread = read(fd, ptr, nleft)) < 0) {
                if (errno == EINTR)
                    nread = 0;
                else
                    return -1;
            } else if (nread == 0)
                break;
    
            nleft -= nread;
            ptr += nread;
        }
        return n - nleft;
    }
    
    ssize_t Writen(int fd, const void *vptr, size_t n)
    {
        size_t nleft;
        ssize_t nwritten;
        const char *ptr;
    
        ptr = vptr;
        nleft = n;
        while (nleft > 0) {
            if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
                if (nwritten < 0 && errno == EINTR)
                    nwritten = 0;
                else
                    return -1;
            }
    
            nleft -= nwritten;
            ptr += nwritten;
        }
        return n;
    }
    static ssize_t my_read(int fd, char *ptr)
    {
        static int read_cnt;
        static char *read_ptr;
        static char read_buf[100];
    
        if (read_cnt <= 0) {
    again:
            if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
                if (errno == EINTR)
                    goto again;
                return -1;
            } else if (read_cnt == 0)
                return 0;
            read_ptr = read_buf;
        }
        read_cnt--;
        *ptr = *read_ptr++;
        return 1;
    }
    
    ssize_t Readline(int fd, void *vptr, size_t maxlen)
    {
        ssize_t n, rc;
        char    c, *ptr;
    
        ptr = vptr;
        for (n = 1; n < maxlen; n++) {
            if ( (rc = my_read(fd, &c)) == 1) {
                *ptr++ = c;
                if (c  == '\n')
                    break;
            } else if (rc == 0) {
                *ptr = 0;
                return n - 1;
            } else
                return -1;
        }
        *ptr  = 0;
        return n;
    }
    /* wrap.h */
    #ifndef __WRAP_H_
    #define __WRAP_H_
    void perr_exit(const char *s);
    int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
    void Bind(int fd, const struct sockaddr *sa, socklen_t salen);
    void Connect(int fd, const struct sockaddr *sa, socklen_t salen);
    void Listen(int fd, int backlog);
    int Socket(int family, int type, int protocol);
    ssize_t Read(int fd, void *ptr, size_t nbytes);
    ssize_t Write(int fd, const void *ptr, size_t nbytes);
    void Close(int fd);
    ssize_t Readn(int fd, void *vptr, size_t n);
    ssize_t Writen(int fd, const void *vptr, size_t n);
    static ssize_t my_read(int fd, char *ptr);
    ssize_t Readline(int fd, void *vptr, size_t maxlen);
    #endif
    
    -------------Makefile
    all:
        gcc wrap.c -c
        gcc server.c -c
        gcc client.c -c
        gcc wrap.o server.o -o server
        gcc wrap.o client.o -o client
    
    .PHONY:clean
    
    clean:
        rm server client *.o
    
    

    3.高并发服务器

    并发服务器开发
    3.1多进程并发服务器

    使用多进程并发服务器时要考虑以下几点:

    1. 父最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
    2. 系统内创建进程个数(内存大小相关)
    3. 进程创建过多是否降低整体服务性能(进程调度)
    3.1.1server
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <string.h>
    #include <strings.h>
    #include <stdlib.h>
    #include <ctype.h>
    #include <signal.h>
    
    #include "wrap.h"
    
    #define SERV_PORT 8000
    
    void do_sig(int num)
    {
        while (waitpid(0, NULL, WNOHANG) > 0)
            ;
    }
    int main(int argc, char *argv[])
    {
        int lfd, cfd, len, i;
        int serv_port = SERV_PORT;
        char buf[1024], client_ip[128];
        struct sockaddr_in serv_addr, client_addr;
        socklen_t client_len;
        pid_t pid;
    
        //子进程结束时传出一个信号,用sigaction类注册信号并接受
        struct sigaction act;
    
        act.sa_handler = do_sig;//信号处理函数do_sig
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);
    
        sigaction(SIGCHLD, &act, NULL);//注册信号
    
        if (argc == 2) 
            serv_port = atoi(argv[1]);
    
        lfd = Socket(AF_INET, SOCK_STREAM, 0);
    
        bzero(&serv_addr, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons((short)serv_port);
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        Bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    
        Listen(lfd, 128);
    
        printf("wait for connect...\n");
    
        while (1) {
            client_len = sizeof(client_addr);
            cfd = Accept(lfd, (struct sockaddr *)&client_addr, &client_len);
            printf("client:%s\t%d\n",
                    inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
                    ntohs(client_addr.sin_port));
            pid = fork();
            if (pid == 0) {
                //in child 
                Close(lfd);
                while (1) {
                    len = Read(cfd, buf, sizeof(buf));
                    if (len <= 0)
                        break;
                    Write(STDOUT_FILENO, buf, len);
                    for (i = 0; i < len; ++i)
                        buf[i] = toupper(buf[i]);
                    Write(cfd, buf, len);
                }
                Close(cfd);
                return 0;
            } else if (pid > 0) {
                //in parent 
                Close(cfd);
            } else {
                perror("fork");
                exit(1);
            }
        }
        Close(lfd);
    
        return 0;
    }
    
    3.1.2client
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <string.h>
    #include <arpa/inet.h>
    
    #define SERV_PORT 8000
    
    int main(int argc, char *argv[])
    {
        int sfd, len;
        struct sockaddr_in serv_addr;
        char buf[4096];
    
        if (argc < 2) {
            printf("./client serv_ip\n");
            return 1;
        }
    
        sfd = socket(AF_INET, SOCK_STREAM, 0);
    
        bzero(&serv_addr, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(SERV_PORT);
        inet_pton(AF_INET, argv[1], &serv_addr.sin_addr.s_addr);
    
        connect(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    
        while (fgets(buf, sizeof(buf), stdin)) {
            write(sfd, buf, strlen(buf));
            len = read(sfd, buf, sizeof(buf));
            write(STDOUT_FILENO, buf, len);
        }
        return 0;
    }
    
    3.2多线程并发服务器

    在使用线程模型开发服务器时需考虑以下问题:

    1. 调整进程内最大文件描述符上限
    2. 线程如有共享数据,考虑线程同步
    3. 服务于客户端线程退出时,退出处理。(退出值,分离态)
    4. 系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
    3.2.1server
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <string.h>
    #include <strings.h>
    #include <stdlib.h>
    #include <ctype.h>
    #include <signal.h>
    
    #include "wrap.h"
    
    #define SERV_PORT 8000
    
    void *do_work(void *arg)
    {
        char buf[1024];
        int len, i;
        int cfd = (int)arg;
        //int pthread_detach(pthread_t phread)
        pthread_detach(pthread_self());//把进程编程分离态https://blog.csdn.net/github_33736971/article/details/51457415
                                       //pthread_self();获取自己的pid
        while (1) {
            len = Read(cfd, buf, sizeof(buf));
            if (len <= 0)
                break;
            Write(STDOUT_FILENO, buf, len);
            for (i = 0; i < len; ++i)
                buf[i] = toupper(buf[i]);
            Write(cfd, buf, len);
        }
        Close(cfd);
        return 0;//只能调return 0 不能调exit 0 不然就是终止进程了
    }
    
    int main(int argc, char *argv[])
    {
        int lfd, cfd;
        int serv_port = SERV_PORT;
        char client_ip[128];
        struct sockaddr_in serv_addr, client_addr;
        socklen_t client_len;
        pthread_t tid;
    
        if (argc == 2) 
            serv_port = atoi(argv[1]);
    
        lfd = Socket(AF_INET, SOCK_STREAM, 0);
    
        bzero(&serv_addr, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons((short)serv_port);
        serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        Bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    
        Listen(lfd, 128);
    
        printf("wait for connect...\n");
    
        while (1) {
            client_len = sizeof(client_addr);
            cfd = Accept(lfd, (struct sockaddr *)&client_addr, &client_len);
            printf("client:%s\t%d\n",
                    inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
                    ntohs(client_addr.sin_port));
            /*
             int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                              void *(*start_routine) (void *), void *arg);
            参数一:线程id,传入传出参数
            参数二:线程初始化参数列表 NULL表示默认
            参数三:线程任务函数
            参数四:参数三的传入传出参数,需要强转(void*)
            */
            pthread_create(&tid, NULL, do_work, (void *)cfd);
        }
    
        return 0;
    }
    
    3.2.2client
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <string.h>
    #include <arpa/inet.h>
    
    #define SERV_PORT 8000
    
    int main(int argc, char *argv[])
    {
        int sfd, len;
        struct sockaddr_in serv_addr;
        char buf[4096];
    
        if (argc < 2) {
            printf("./client serv_ip\n");
            return 1;
        }
    
        sfd = socket(AF_INET, SOCK_STREAM, 0);
    
        bzero(&serv_addr, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_port = htons(SERV_PORT);
        inet_pton(AF_INET, argv[1], &serv_addr.sin_addr.s_addr);
    
        connect(sfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    
        while (fgets(buf, sizeof(buf), stdin)) {
            write(sfd, buf, strlen(buf));
            len = read(sfd, buf, sizeof(buf));
            write(STDOUT_FILENO, buf, len);
        }
        return 0;
    }
    
    3.2.3Makefile
    all:
        gcc wrap.c -c                    #多个二进制文件连接成一个app
        gcc server.c -c                  #首先变成默认的.o文件 再多文件连接
        gcc client.c -c
        gcc wrap.o server.o -lpthread -o server  #连接线程库,直接在连接处加上-lphread
        gcc wrap.o client.o -o client
    
    .PHONY:clean
    
    clean:
        rm server client *.o
    

    4.总结

    1.png

    相关文章

      网友评论

        本文标题:六、TCP详解和并发服务器

        本文链接:https://www.haomeiwen.com/subject/izoemftx.html