歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux管理 >> Linux網絡 >> linux網絡編程之socket(三) 最簡單的回射客戶/服務器程序

linux網絡編程之socket(三) 最簡單的回射客戶/服務器程序

日期:2017/3/3 16:26:08   编辑:Linux網絡

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

echoser.c 程序的功能是從客戶端讀取字符 然後直接回射回去。

/*************************************************************************
    > File Name: echoser.c
    > Author: Simba
    > Mail: [email protected]
    > Created Time: Fri 01 Mar 2013 06:15:27 PM CST
 ************************************************************************/
    
#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));
    
    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;
}

下面介紹程序中用到的socket API,這些函數都在sys/socket.h中。

int socket(int family, int type, int protocol);

socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣返回一個文件描述符,應用程序可以像讀 寫文件一樣用read/write在網絡上收發數據,如果socket()調用出錯則返回-1。對於IPv4,family參數指定為AF_INET。對 於TCP協議,type參數指定為SOCK_STREAM,表示面向流的傳輸協議。如果是UDP協議,則type參數指定為SOCK_DGRAM,表示 面向數據報的傳輸協議。protocol參數的介紹從略,指定為0即可。

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序 的地址和端口號後就可以向服務器發起連接,因此服務器需要調用bind綁定一個固定的網絡地址和端口號。bind()成功返回 0,失敗返回-1。

bind()的作用是將參數sockfd和myaddr綁定在一起,使sockfd這個用於網絡通訊的文件描述符監聽 myaddr所描述的地址和端口號。struct sockaddr *是一個通用指針類型,myaddr參數實際上可以接受多種協議的sockaddr 結構體,而它們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度。我們的程序中對myaddr參數是這樣初始 化的:

memset(&servaddr, 0, sizeof(servaddr));

servaddr.sin_family = AF_INET;

servaddr.sin_port = htons(5188);

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

首先將整個結構體清零(也可以用bzero函數),然後設置地址類型為AF_INET,網絡地址為INADDR_ANY,這個宏表 示本地的任意IP地址,因為服務器可能有多個網卡,每個網卡也可能綁定多個IP地址,這樣設置可以在所有的IP地址上監聽 ,直到與某個客戶端建立了連接時才確定下來到底用哪個IP地址,端口號為5188。

int listen(int sockfd, int backlog);

典型的服務器程序可以同時服務於多個客戶端,當有客戶端發起連接時,服務器調用的accept()返回並接受這 個連接,如果有大量的客戶端發起連接而服務器來不及處理,尚未accept的客戶端就處於連接等待狀態,listen()聲明 sockfd處於監聽狀態,並且最多允許有backlog個客戶端處於連接等待狀態,如果接收到更多的連接請求就忽略。listen() 成功返回0,失敗返回-1。

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

三方握 手完成後,服務器調用accept()接受連接,如果服務器調用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端 連接上來。cliaddr是一個傳出參數,accept()返回時傳出客戶端的地址和端口號。addrlen參數是一個傳入傳出參數 (value-result argument),傳入的是調用者提供的緩沖區cliaddr的長度以避免緩沖區溢出問題,傳出的是客戶端地址結 構體的實際長度(有可能沒有占滿調用者提供的緩沖區)。如果給cliaddr參數傳NULL,表示不關心客戶端的地址。

在上面的程序中我們通過peeraddr打印連接上來的客戶端ip和端口號。

在while循環中從accept返回的文件描述符 conn讀取客戶端的請求,然後直接回射回去。

echocli.c 的作用是從標准輸入得到一行字符,然後發送給服務器後 從服務器接收,再打印在標准輸出。

/*************************************************************************
    > File Name: echoser.c
    > Author: Simba
    > Mail: [email protected]
    > Created Time: Fri 01 Mar 2013 06:15:27 PM CST
 ************************************************************************/
    
    
#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");
    
    
    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(),內核會 自動給服務器分配監聽端口,每次啟動服務器時端口號都不一樣,客戶端要連接服務器就會遇到麻煩。

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

客戶端需要調用connect()連接服務器 ,connect和bind的參數形式一致,區別在於bind的參數是自己的地址,而connect的參數是對方的地址。connect()成功返 回0,出錯返回-1。

先編譯運行服務器:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser

然後在另一個終端裡用netstat命令查看:

simba@ubuntu:~$ netstat -anp | grep 5188

(Not all processes could be identified, non-owned process info

will not be shown, you would have to be root to see it all.)

tcp 0 0 0.0.0.0:5188 0.0.0.0:* LISTEN 4425/echoser

可以看到server程序監聽5188端口,IP地址還沒確定下來。現在編譯運行客戶端:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echocli

回到server所在的終端,看 看server的輸出:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser

recv connect ip=127.0.0.1 port=59431

可見客戶端的端口號是自動分配的。

再次netstat 一下

simba@ubuntu:~$ netstat -anp | grep 5188

(Not all processes could be identified, non-owned process info

will not be shown, you would have to be root to see it all.)

tcp 0 0 0.0.0.0:5188 0.0.0.0:* LISTEN 4425/echoser

tcp 0 0 127.0.0.1:59431 127.0.0.1:5188 ESTABLISHED 4852/echocli

tcp 0 0 127.0.0.1:5188 127.0.0.1:59431 ESTABLISHED 4425/echoser

應用程序中的一個socket文件描述符對應一個socket pair,也就是源地址:源端口號和目的地 址:目的端口號,也對應一個TCP連接。

上面第一行即echoser.c 中的listenfd;第二行即echocli 中的conn; 第三 行即echoser.c 中的sock。4425和4852分別是進程id。

現在來做個測試,先把40~42行的代碼注釋起來。

首 先啟動server,然後啟動client,然後用Ctrl-C使server終止,這時馬上再運行server,結果是:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser

bind error: Address already in use

這是因為,雖然server的應用程序終止了,但TCP協議層的連接並沒有完全斷開,因此不能再次監聽同樣的 server端口。我們用netstat命令查看一下:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ netstat -anp | grep 5188

(Not all processes could be identified, non-owned process info

will not be shown, you would have to be root to see it all.)

tcp 0 0 127.0.0.1:5188 127.0.0.1:37381 FIN_WAIT2 -

tcp 1 0 127.0.0.1:37381 127.0.0.1:5188 CLOSE_WAIT 2302/echocli

server終止時,socket描述符會自動關閉並發FIN段給client,client收到 FIN後處於CLOSE_WAIT狀態,但是client並沒有終止,也沒有關閉socket描述符,因此不會發FIN給server,因此server的 TCP連接處於FIN_WAIT2狀態。

現在用Ctrl-C把client也終止掉,再觀察現象:

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ netstat -anp | grep 5188

(No info could be read for "-p": geteuid()=1000 but you should be root.)

tcp 0 0 127.0.0.1:5188 127.0.0.1:37382 TIME_WAIT -

simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser

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:8000)沒有完全斷開,而我們重新監聽的是listenfd(0.0.0.0:8000), 雖然是占用同一個端口,但IP地址不同,connfd對應的是與某個客戶端通訊的一個具體的IP地址,而listenfd對應的是 wildcard address。解決這個問題的方法是使用setsockopt()設置socket描述符的選項SO_REUSEADDR為1,表示允許創建端 口號相同但IP地址不同的多個socket描述符。將原來注釋的40~42行代碼打開,問題解決。

Copyright © Linux教程網 All Rights Reserved