UNIX網絡編程——TCP回射服務器/客戶端程序

下面經過最簡單的客戶端/服務器程序的實例來學習socket API。ubuntu

                           

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(),內核會自動給服務器分配監聽端口,每次啓動服務器時端口號都不同,客戶端要鏈接服務器就會遇到麻煩。

     先編譯運行服務器:
socket

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地址還沒肯定下來。如今編譯運行客戶端:
tcp

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。spa


     如今來作個測試,先serv.c中的把33~35行的代碼註釋掉。
操作系統

     首先啓動server,而後啓動client,而後用Ctrl-C使server終止,這時立刻再運行server,結果是:code

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
相關文章
相關標籤/搜索