歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Unix知識 >> Unix基礎知識 >> UNIX網絡編程:TCP回射服務器/客戶端程序

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

日期:2017/3/3 14:55:43   编辑:Unix基礎知識

下面通過最簡單的客戶端/服務器程序的實例來學習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;  
}

查看本欄目更多精彩內容:http://www.bianceng.cn/OS/unix/

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

查看本欄目更多精彩內容:http://www.bianceng.cn/OS/unix/

先編譯運行服務器:

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
Copyright © Linux教程網 All Rights Reserved