• UNIX网络编程——TCP回射服务器/客户端程序


    下面通过最简单的客户端/服务器程序的实例来学习socket API。

                               

    serv.c 程序的功能是从客户端读取字符然后直接回射回去:

    #include<stdio.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<unistd.h>
    #include<stdlib.h>
    #include<errno.h>
    #include<arpa/inet.h>
    #include<netinet/in.h>
    #include<string.h>
    
    #define ERR_EXIT(m) 
        do { 
            perror(m); 
            exit(EXIT_FAILURE); 
        } while (0)
    
    
    int main(void)
    {
        int listenfd; //被动套接字(文件描述符),即只可以accept
        if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
            //  listenfd = socket(AF_INET, SOCK_STREAM, 0)
            ERR_EXIT("socket error");
    
        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 error");
    
        if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("bind error");
    
        if (listen(listenfd, SOMAXCONN) < 0) //listen应在socket和bind之后,而在accept之前
            ERR_EXIT("listen error");
    
        struct sockaddr_in peeraddr; //传出参数
        socklen_t peerlen = sizeof(peeraddr); //传入传出参数,必须有初始值
        int conn; // 已连接套接字(变为主动套接字,即可以主动connect)
        if ((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept error");
        printf("recv connect ip=%s port=%d
    ", inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
    	struct sockaddr_in localaddr;
    	char serv_ip[20];
    	socklen_t local_len = sizeof(localaddr);
    	memset(&localaddr, 0, sizeof(localaddr));
    	if( getsockname(conn,(struct sockaddr *)&localaddr,&local_len) != 0 )
    	    ERR_EXIT("getsockname error");
    	inet_ntop(AF_INET, &localaddr.sin_addr, serv_ip, sizeof(serv_ip));
    	printf("host %s:%d
    ", serv_ip, ntohs(localaddr.sin_port)); 
    
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            fputs(recvbuf, stdout);
            write(conn, recvbuf, ret);
        }
    
        close(conn);
        close(listenfd);
    
        return 0;
    }


    cli.c 的作用是从标准输入得到一行字符,然后发送给服务器后从服务器接收,再打印在标准输出:

    #include<stdio.h>
    #include<sys/types.h>
    #include<sys/socket.h>
    #include<unistd.h>
    #include<stdlib.h>
    #include<errno.h>
    #include<arpa/inet.h>
    #include<netinet/in.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)
            //  listenfd = socket(AF_INET, SOCK_STREAM, 0)
            ERR_EXIT("socket error");
    
    
        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");
        /* inet_aton("127.0.0.1", &servaddr.sin_addr); */
    
        if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("connect error");
        struct sockaddr_in localaddr;
    	char cli_ip[20];
    	socklen_t local_len = sizeof(localaddr);
    	memset(&localaddr, 0, sizeof(localaddr));
    	if( getsockname(sock,(struct sockaddr *)&localaddr,&local_len) != 0 )
    	    ERR_EXIT("getsockname error");
    	inet_ntop(AF_INET, &localaddr.sin_addr, cli_ip, sizeof(cli_ip));
    	printf("host %s:%d
    ", cli_ip, ntohs(localaddr.sin_port)); 
    
        char sendbuf[1024] = {0};
        char recvbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
    
            write(sock, sendbuf, strlen(sendbuf));
            read(sock, recvbuf, sizeof(recvbuf));
    
    
            fputs(recvbuf, stdout);
    
            memset(sendbuf, 0, sizeof(sendbuf));
            memset(recvbuf, 0, sizeof(recvbuf));
        }
    
    
        close(sock);
    
    
        return 0;
    }

         由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

         先编译运行服务器:

    huangcheng@ubuntu:~$./serv

         然后在另一个终端里用netstat命令查看:

    huangcheng@ubuntu:~$ netstat -anp | grep 5188
    (并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
    tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      2998/serv
         

         可以看到server程序监听5188端口,IP地址还没确定下来。现在编译运行客户端:

    huangcheng@ubuntu:~$ ./cli

         回到server所在的终端,看看server的输出:

    huangcheng@ubuntu:~$ ./serv
    recv connect ip=127.0.0.1 port=42107

         可见客户端的端口号是自动分配的。再次netstat 一下:

    huangcheng@ubuntu:~$ netstat -anp | grep 5188
    (并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
    tcp        0      0 0.0.0.0:5188            0.0.0.0:*               LISTEN      2998/serv   
    tcp        0      0 127.0.0.1:5188          127.0.0.1:42107         ESTABLISHED 2998/serv   
    tcp        0      0 127.0.0.1:42107         127.0.0.1:5188          ESTABLISHED 3198/cli    

          应用程序中的一个socket文件描述符对应一个socket pair,也就是源地址:源端口号和目的地址:目的端口号,也对应一个TCP连接。

         上面第一行即serv.c 中的listenfd;第二行即serv.c 中的sock; 第三行即cli 中的conn。2998和3198分别是进程id。


         现在来做个测试,先serv.c中的把33~35行的代码注释掉。

         首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server,结果是:

    huangcheng@ubuntu:~$ ./serv
    bind error: Address already in use
         这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:
    huangcheng@ubuntu:~$ netstat -anp | grep 5188
    (并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换           到 root 用户)
    tcp        0      0 127.0.0.1:5188          127.0.0.1:42108         FIN_WAIT2   -                      
    tcp        1      0 127.0.0.1:42108         127.0.0.1:5188          CLOSE_WAIT  3260/cli               
         server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态。


         现在用Ctrl-C把client也终止掉,再观察现象:

    huangcheng@ubuntu:~$ netstat -anp | grep 5188
    (并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
    tcp        0      0 127.0.0.1:5188          127.0.0.1:42108         TIME_WAIT   -
    
    huangcheng@ubuntu:~$ ./serv
    bind error: Address already in use

         client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximumsegment lifetime)的时间后才能回到CLOSED状态,需要有MSL 时间的主要原因是在这段时间内如果最后一个ack段没有发送给对方,则可以重新发送因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。至于为什么要规定TIME_WAIT的时间请大家参考UNP 2.7节。


         在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为,TCP连接没有完全断开指的是connfd(127.0.0.1:5188)没有完全断开,而我们重新监听的是listenfd(0.0.0.0:5188),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1表示允许创建端口号相同但IP地址不同的多个socket描述符。将原来注释的33~35行代码打开,问题解决。


         先运行服务器,在运行客户端:

    huangcheng@ubuntu:~$ ./serv
    recv connect ip=127.0.0.1 port=42107
    host 127.0.0.1:5188
    huangcheng
    ctt
    huangcheng@ubuntu:~$ ./cli
    host 127.0.0.1:42107
    huangcheng
    huangcheng
    ctt
    ctt


  • 相关阅读:
    Delphi-基础(for循环)
    Delphi-基础(运算符)
    Delphi-基础
    python 序列化
    python 模块、包
    time,datetime,random,os,sys,hashlib,logging,configparser,re模块
    第一章.Python环境安装&Pycharm日常使用
    第一章.变量.常量.注释.运算符.if流程控制
    Redis持久化数据介绍&主从复制和哨兵模式环境部署
    Redis缓存应用安装部署&常见的数据类型
  • 原文地址:https://www.cnblogs.com/hehehaha/p/6332606.html
Copyright © 2020-2023  润新知