zoukankan      html  css  js  c++  java
  • linux网络编程之socket编程(三)

    今天继续对socket编程进行学习,在学习之前,需要回顾一下上一篇中编写的回射客户/服务器程序(http://www.cnblogs.com/webor2006/p/3923254.html),因为今天的知识点需要基于它来进行说明,下面来回顾一下关键代码:

    对于服务器端:echosrv.c

    对于客户端:echocli.c

    下面通过一个简单的图来描述一下其关系:

    可想而知,这两个套接字都有自己的地址,对于conn服务端而言,它的地址是在绑定的时候确认的,也就是:

    而对于sock客户端而言,它的地址是在连接成功时确定的。一旦连接成功,则会自动选择一个本机的地址和端口号,当一个客户端连接服务器成功时,在服务器端是可以打印出客户端的地址和端口信息的,具体代码如下:

    所以可以将其客户端的地址和端口号打印出来,修改服务端代码如下:

    这次编译运行看下效果:

    当客户端连接成功时,则在服务端将其客户端的ip和端口号打印出来了。

    接下来,要解决一个上篇博文中遇到的问题,问题现象就是如下:

    原因是由于,重启时会重新再绑定,而此时该服务器是处于TIME_WAIT状态,通过命令可以查看到该状态:

    【注意】:TIME_WAIT状态,需服务器与客户端都已经退出来才会出来。

    而该状态下,默认是无法再次绑定的,那如何解决此问题呢?可以使用SO_REUSEADDR选项来解决此问题:

    具体使用方法如下,修改服务端代码如下:

    这时再来看是否解决了此问题:

    但是,这个错误还会在一种场景下报出来,如下:

    下面再来看一下目前程序的问题:目前服务器只能接收一个客户端的连接,看下面:

     

    分析一下服务端的代码就可以得知:

    解决这个问题的思路就是:一个连接一个进程来处理并发(process-per-connection),也就是上面画红圈的放到子进程去处理,然后主进程可以去accept客户端的请求了,具体代码修改如下:

    echosrv.c:

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    #define ERR_EXIT(m) 
            do 
            { 
                    perror(m); 
                    exit(EXIT_FAILURE); 
            } while(0)
    
    void do_service(int conn)//将处理客户端请求数据逻辑封装到一个函数中,这样代码更加模块化
    {
        char recvbuf[1024];
            while (1)
            {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = read(conn, recvbuf, sizeof(recvbuf));
                fputs(recvbuf, stdout);
                write(conn, recvbuf, ret);
            }
    }
    
    int main(void)
    {
        int listenfd;
        if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
    /*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
            ERR_EXIT("socket");
    
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
        /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/
    
        int on = 1;
        if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
            ERR_EXIT("setsockopt");
    
        if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("bind");
        if (listen(listenfd, SOMAXCONN) < 0)
            ERR_EXIT("listen");
    
        struct sockaddr_in peeraddr;
        socklen_t peerlen = sizeof(peeraddr);
        int conn;
        
        pid_t pid;
        while (1)//用一个循环来不断处理客户端的请求,所以将accept操作也放到循环中
        {
            if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
                ERR_EXIT("accept");
    
            printf("ip=%s port=%d
    ", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
    
            pid = fork();//创建进程
            if (pid == -1)
                ERR_EXIT("fork");
            if (pid == 0)
            {//子进程来处理与客户端的数据
                close(listenfd);//对于子进程而言,监听listenfd套接字没用,则直接关闭掉,注意:套接字在父子进程是共享的
                do_service(conn);//开始处理请求数据
            }
            else//父进程则回到while循环头,去accept新的客户端的请求,这样就比较好的解决多个客户端的请求问题
                close(conn);
        }
        
        return 0;
    }

    下面来看下效果:

    对于这段程序,其实还需要完善,也就是不能监听客户端的退出,

    那怎么监听客户端的监听呢?

    编译运行看一下效果:

    从中可以看到,当客户端退出时,服务端也收到消息了。

    下面用多进程方式实现点对点聊天来进一步理解套接字编程,这里实现的聊天程序是这样:

    那不管对于服务端,还是客户端,应该每个端点都有有两个进程,一个进程读取对等方的数据,另一个进程专门从键盘中接收数据发送给对待方,这里实现的只是一个服务端跟一个客户端的通讯,不考虑一个服务端跟多个客户端的通讯了,重在练习,下面还是基于之前的代码开始实现:

    p2psrv.c【点对点通信服务端】:

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <signal.h>
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    #define ERR_EXIT(m) 
            do 
            { 
                    perror(m); 
                    exit(EXIT_FAILURE); 
            } while(0)
    
    int main(void)
    {
        int listenfd;
        if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
    /*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
            ERR_EXIT("socket");
    
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
        /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/
    
        int on = 1;
        if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
            ERR_EXIT("setsockopt");
    
        if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("bind");
        if (listen(listenfd, SOMAXCONN) < 0)
            ERR_EXIT("listen");
    
        struct sockaddr_in peeraddr;
        socklen_t peerlen = sizeof(peeraddr);
        int conn;
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");
    
        printf("ip=%s port=%d
    ", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
    
    
        pid_t pid;
        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        
        if (pid == 0)
        {//子进程用来从键盘中获取输入输数据向客户端发送数据
            char sendbuf[1024] = {0};
            while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
            {
                write(conn, sendbuf, strlen(sendbuf));
                memset(sendbuf, 0, sizeof(sendbuf));
            }
            exit(EXIT_SUCCESS);
        }
        else
        {//父进程读取从客户端发送过来的数据
            char recvbuf[1024];
            while (1)
            {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = read(conn, recvbuf, sizeof(recvbuf));
                if (ret == -1)
                    ERR_EXIT("read");
                else if (ret == 0)
                {
                    printf("peer close
    ");
                    break;
                }
                
                fputs(recvbuf, stdout);
            }
            exit(EXIT_SUCCESS);
        }
        
        return 0;
    }

    基本上前面的listen、bind、accept的代码没动,编译一下:

    木有问题~~~ 

    p2pcli.c【点对点通信客户端】:

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <signal.h>
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    #define ERR_EXIT(m) 
            do 
            { 
                    perror(m); 
                    exit(EXIT_FAILURE); 
            } while(0)
    
    
    int main(void)
    {
        int sock;
        if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
            ERR_EXIT("socket");
    
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
        if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("connect");
    
        pid_t pid;
        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
    
        if (pid == 0)
        {//子进程接收来自服务端发来的数据
            char recvbuf[1024];
            while (1)
            {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = read(sock, recvbuf, sizeof(recvbuf));
                if (ret == -1)
                    ERR_EXIT("read");
                else if (ret == 0)
                {
                    printf("peer close
    ");
                    break;
                }
                
                fputs(recvbuf, stdout);
            }
            close(sock);
        }
        else
        {//父进程获取从键盘中敲入的命令向服务端发送数据
            char sendbuf[1024] = {0};
            while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
            {
                write(sock, sendbuf, strlen(sendbuf));
                memset(sendbuf, 0, sizeof(sendbuf));
            }
            close(sock);
        }
        return 0;
    }

    编译运行:

    可见就实现了点对点的聊天程序。这时客户端关闭时,服务端也关闭了,但是,实际上服务端的程序还是有些问题的,分析一下代码:

    可以在父子进程退出时都打印一个log来验证下:

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <signal.h>
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    #define ERR_EXIT(m) 
            do 
            { 
                    perror(m); 
                    exit(EXIT_FAILURE); 
            } while(0)
    
    int main(void)
    {
        int listenfd;
        if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
    /*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
            ERR_EXIT("socket");
    
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
        /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/
    
        int on = 1;
        if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
            ERR_EXIT("setsockopt");
    
        if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("bind");
        if (listen(listenfd, SOMAXCONN) < 0)
            ERR_EXIT("listen");
    
        struct sockaddr_in peeraddr;
        socklen_t peerlen = sizeof(peeraddr);
        int conn;
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");
    
        printf("ip=%s port=%d
    ", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
    
    
        pid_t pid;
        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        
        if (pid == 0)
        {
            char sendbuf[1024] = {0};
            while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
            {
                write(conn, sendbuf, strlen(sendbuf));
                memset(sendbuf, 0, sizeof(sendbuf));
            }
            printf("child close
    ");
            exit(EXIT_SUCCESS);
        }
        else
        {
            char recvbuf[1024];
            while (1)
            {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = read(conn, recvbuf, sizeof(recvbuf));
                if (ret == -1)
                    ERR_EXIT("read");
                else if (ret == 0)
                {
                    printf("peer close
    ");
                    break;
                }
                
                fputs(recvbuf, stdout);
            }
            printf("parent close
    ");
            exit(EXIT_SUCCESS);
        }
        
        return 0;
    }

    而此时,如果我按任意一个字符,则子进程就退出来:

    那怎么解决当父进程退出时,也让其子进程退出呢,就可以用到我们之前学过的信号来解决了,具体如下:

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <signal.h>
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    #define ERR_EXIT(m) 
            do 
            { 
                    perror(m); 
                    exit(EXIT_FAILURE); 
            } while(0)
    
    void handler(int sig)//当子进程收到信号时,则将自身退出
    {
        printf("recv a sig=%d
    ", sig);
        exit(EXIT_SUCCESS);
    }
    
    int main(void)
    {
        int listenfd;
        if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
    /*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
            ERR_EXIT("socket");
    
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
        /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/
    
        int on = 1;
        if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
            ERR_EXIT("setsockopt");
    
        if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("bind");
        if (listen(listenfd, SOMAXCONN) < 0)
            ERR_EXIT("listen");
    
        struct sockaddr_in peeraddr;
        socklen_t peerlen = sizeof(peeraddr);
        int conn;
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");
    
        printf("ip=%s port=%d
    ", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
    
    
        pid_t pid;
        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        
        if (pid == 0)
        {
            signal(SIGUSR1, handler);//子进程注册一个SIGUSR1信号,以便在父进程退出时,通知子进程退出
            char sendbuf[1024] = {0};
            while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
            {
                write(conn, sendbuf, strlen(sendbuf));
                memset(sendbuf, 0, sizeof(sendbuf));
            }
            printf("child close
    ");
            exit(EXIT_SUCCESS);
        }
        else
        {
            char recvbuf[1024];
            while (1)
            {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = read(conn, recvbuf, sizeof(recvbuf));
                if (ret == -1)
                    ERR_EXIT("read");
                else if (ret == 0)
                {
                    printf("peer close
    ");
                    break;
                }
                
                fputs(recvbuf, stdout);
            }
            printf("parent close
    ");
            kill(pid, SIGUSR1);//当父进程退出时,发送一个SIGUSR1信号
            exit(EXIT_SUCCESS);
        }
        
        return 0;
    }

    这时再看下效果:

    如果反过来呢,将服务端关闭,那客户端程序也会关闭么?

    可以看到,当服务端退出时,客户端并没有退出,那是啥原因呢?

    那解决此问题也是可以利用信号来解决,就像服务端一样,可以在子进程退出时,向父进程发送一个信号,然后父进程也退出既可,修改代码如下:

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <signal.h>
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    #define ERR_EXIT(m) 
            do 
            { 
                    perror(m); 
                    exit(EXIT_FAILURE); 
            } while(0)
    
    void handler(int sig)
    {
        printf("recv a sig=%d
    ", sig);
        exit(EXIT_SUCCESS);
    }
    
    int main(void)
    {
        int sock;
        if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
            ERR_EXIT("socket");
    
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
        if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("connect");
    
        pid_t pid;
        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
    
        if (pid == 0)
        {
            char recvbuf[1024];
            while (1)
            {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = read(sock, recvbuf, sizeof(recvbuf));
                if (ret == -1)
                    ERR_EXIT("read");
                else if (ret == 0)
                {
                    printf("peer close
    ");
                    break;
                }
                
                fputs(recvbuf, stdout);
            }
            close(sock);
            kill(getppid(), SIGUSR1);
        }
        else
        {
            signal(SIGUSR1, handler);
            char sendbuf[1024] = {0};
            while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
            {
                write(sock, sendbuf, strlen(sendbuf));
                memset(sendbuf, 0, sizeof(sendbuf));
            }
            close(sock);
        }
    
    
        
        return 0;
    }

    这里就不多解释了,跟服务端的道理一样,这样再来运行一下看下效果:

    这样,就实现了一个点对点的聊天的程序,当然,这里没有考虑到多个客户端与服务端通讯的问题,但是根据这个程序也能容易进行扩展,今天的内容就先学到这,下次再见~~~

  • 相关阅读:
    通过HttpListener实现简单的Http服务
    WCF心跳判断服务端及客户端是否掉线并实现重连接
    NHibernate初学六之关联多对多关系
    NHibernate初学五之关联一对多关系
    EXTJS 4.2 资料 跨域的问题
    EXTJS 4.2 资料 控件之Grid 那些事
    EXTJS 3.0 资料 控件之 GridPanel属性与方法大全
    EXTJS 3.0 资料 控件之 Toolbar 两行的用法
    EXTJS 3.0 资料 控件之 combo 用法
    EXTJS 4.2 资料 控件之 Store 用法
  • 原文地址:https://www.cnblogs.com/webor2006/p/3932917.html
Copyright © 2011-2022 走看看