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\n", 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\n", 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\n", 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


posted on 2013-07-23 08:42  胡永光  阅读(175)  评论(0编辑  收藏  举报

导航