linux網絡程序設計——2 網絡模型

2 C/S模型-TCP

大部分進程間通信使用客戶端-服務器模型。進程間通信指的是兩個進程之間相互通信,其中,客戶端進程連接服務器進程,通常是發出數據請求。一個很好的比喻是一個人給另一個人打電話,打出電話的人就好比客戶端,接電話的人就好比服務器。

有兩點需要注意,第一,客戶端需要知道服務器是否存在,如果服務器存在,服務器的地址是多少;但是在客戶端連接服務器之前,服務器並不需要知道客戶端的地址(甚至客戶端存在與否)。第二,連接一旦建立,客戶端和服務器之間就可以接收和發送數據。

客戶端和服務器用於建立連接的系統調用有所不同,但都包含socket的基本構建。

客戶端建立socket的步驟:

- 使用socket()函數(系統調用)創建一個socket

- 使用connect()函數將socket連接到服務器的地址

- 發送或接收數據。有很多種方法進行發動數據和接收數據,但是最簡單的方法是使用read()和write()函數

服務器端建立socket的步驟:

- 使用socket()函數創建一個socket

- 使用bind()函數將socket和一個地址綁定,對於網絡上的服務器來講,地址包含主機和端口號,像這樣:127.0.0.1:8888

- 使用listen()函數監聽連接

- 當監聽到有連接時,使用accept()函數接受一個連接。這個函數通常會堵塞,直到客戶端連接建立。

- 接收和發送數據

服務器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化後,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到後從connect()返回,同時應答一個ACK段,服務器收到後從accept()返回。

  數據傳輸的過程:

  1) 建立連接後,TCP協議提供全雙工的通信服務,但是一般的客戶端/服務器程序的流程是由客戶端主動發起請求,服務器被動處理請求,一問一答的方式。因此,服務器從accept()返回後立刻調用read(),讀socket就像讀管道一樣,如果沒有數據到達就阻塞等待,這時客戶端調用write()發送請求給服務器,服務器收到後從read()返回,對客戶端的請求進行處理,在此期間客戶端調用read()阻塞等待服務器的應答,服務器調用write()將處理結果發回給客戶端,再次調用read()阻塞等待下一條請求,客戶端收到後從read()返回,發送下一條請求,如此循環下去。

  如果客戶端沒有更多的請求了,就調用close()關閉連接,就像寫端關閉的管道一樣,服務器的read()返回0,這樣服務器就知道客戶端關閉了連接,也調用close()關閉連接。注意,任何一方調用close()後,連接的兩個傳輸方向都關閉,不能再發送數據了。如果一方調用shutdown()則連接處於半關閉狀態,仍可接收對方發來的數據。

在學習socket API時要注意應用程序和TCP協議層是如何交互的: *應用程序調用某個socket函數時TCP協議層完成什麼動作,比如調用connect()會發出SYN段 *應用程序如何知道TCP協議層的狀態變化,比如從某個阻塞的socket函數返回就表明TCP協議收到了某些段,再比如read()返回0就表明收到了FIN段。

實例<客戶端/服務器>

mkdir server_test

touch server.c

touch client.c

touch Makefile

 

server.c

#include <sys/types.h>     

#include <sys/socket.h>

#include <arpa/inet.h>

#include <stdio.h>

#include <string.h>

#include <sys/types.h>

#include <netinet/in.h>

 

#define SERVER_PORT 8000

#define MAXLINE 4096

 

int main(void)

{

    struct sockaddr_in serveraddr, clientaddr;

    int sockfd, addrlen, confd, len, i;

    char ipstr[128];

    char buf[MAXLINE];

 

    //1.socket

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    //2.bind

    bzero(&serveraddr, sizeof(serveraddr));

    /* 地址族協議IPv4 */

    serveraddr.sin_family = AF_INET;

    /* IP地址 */

    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

    serveraddr.sin_port = htons(SERVER_PORT);

    bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    //3.listen

    listen(sockfd, 128);

    while (1) {

        //4.accept阻塞監聽客戶端鏈接請求

        addrlen = sizeof(clientaddr);

        confd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen);//返回的是客戶端和服務端專用通道的socket描述符

        //輸出客戶端IP地址和端口號

        inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, ipstr, sizeof(ipstr));

        printf("client ip %s\tport %d\n",

                inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, ipstr, sizeof(ipstr)),

                ntohs(clientaddr.sin_port));

 

        //和客戶端交互數據操作confd

        //5.處理客戶端請求

        len = read(confd, buf, sizeof(buf));

        i = 0;

        while (i < len) {

            buf[i] = toupper(buf[i]);

            i++;

        }

        write(confd, buf, len);

 

        close(confd);

    }

    close(sockfd);

 

    return 0;

}

 

client.c

#include <netinet/in.h>

#include <stdio.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <arpa/inet.h>

#include <string.h>

#include <stdlib.h>

#include <sys/stat.h>

#include <unistd.h>

#include <fcntl.h>

#define SERVER_PORT 8000

#define MAXLINE 4096

int main(int argc, char *argv[])

{

    struct sockaddr_in serveraddr;

    int confd, len;

    char ipstr[] = "127.0.0.1";

    char buf[MAXLINE];

    if (argc < 2) {

        printf("./client str\n");

        exit(1);

    }

    //1.創建一個socket

    confd = socket(AF_INET, SOCK_STREAM, 0);

    //2.初始化服務器地址

    bzero(&serveraddr, sizeof(serveraddr));

    serveraddr.sin_family = AF_INET;

    //"192.168.6.254"

    inet_pton(AF_INET, ipstr, &serveraddr.sin_addr.s_addr);

    serveraddr.sin_port  = htons(SERVER_PORT);

    //3.鏈接服務器

    connect(confd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

 

    //4.請求服務器處理數據

    write(confd, argv[1], strlen(argv[1]));

    len = read(confd, buf, sizeof(buf));

    write(STDOUT_FILENO, buf, len);

 

    //5.關閉socket

    close(confd);

    return 0;

}

 

Makefile

all:server client

 

server:server.c

    gcc $< -o $@

 

client:client.c

    gcc $< -o $@

 

.PHONY:clean

clean:

    rm -f server

    rm -f client

由於客戶端不需要固定的端口號,因此不必調用bind(),客戶端的端口號由內核自動分配。注意,客戶端不是不允許調用bind(),只是沒有必要調用bind()固定一個端口號,服務器也不是必須調用bind(),但如果服務器不調用bind(),內核會自動給服務器分配監聽端口,每次啓動服務器時端口號都不一樣,客戶端要連接服務器就會遇到麻煩。客戶端和服務器啓動後可以查看鏈接情況:netstat-apn|grep 8000

3 C/S模型-UDP

實例<客戶端/服務器>

udp_server.cpp

#include <iostream>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <signal.h>

#include <unistd.h>

#include <stdlib.h>

#include <assert.h>

#include <stdio.h>

#include <string.h>

 

using namespace std;

 

#define BUFFER_SIZE 1024

 

void tsocket(int argc, const char * argv[]);

 

int main(int argc, const char * argv[]) {

    tsocket(argc,argv);

    return 0;

}

void tsocket(int argc, const char * argv[]){

    if(argc < 3){

        exit(-1);

    }

 

    const char* ip = argv[1];

    int port = atoi(argv[2]);

    int backlog = atoi(argv[3]);

 

    std::cout << "ip=" << ip << " port="<<port << " backlog=" << backlog  << std::endl;

 

    int fd;

    int check_ret;

 

    fd = socket(PF_INET,SOCK_DGRAM , 0);

    assert(fd >= 0);

 

    struct sockaddr_in address;

    bzero(&address,sizeof(address));

 

    //轉換成網絡地址

    address.sin_port = htons(port);

    address.sin_family = AF_INET;

    //地址轉換

    inet_pton(AF_INET, ip, &address.sin_addr);

 

    //綁定ip和端口

    check_ret = bind(fd,(struct sockaddr*)&address,sizeof(address));

    assert(check_ret >= 0);

 

    while(1){

        char buffer[BUFFER_SIZE];

        struct sockaddr_in addressClient;

        socklen_t clientLen = sizeof(addressClient);

        memset(buffer, '\0', BUFFER_SIZE);

        //獲取信息

        if(recvfrom(fd, buffer, BUFFER_SIZE-1,0,(struct sockaddr*)&addressClient, &clientLen) == -1)

        {

           perror("Receive Data Failed:");

           exit(1);

        }

        printf("buffer=%s\n", buffer);

    }

    close(fd);

}

 

udp_client.cpp

#include <iostream>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <signal.h>

#include <unistd.h>

#include <stdlib.h>

#include <assert.h>

#include <stdio.h>

#include <string.h>

 

using namespace std;

 

void tserver(int argc, const char * argv[]);

 

int main(int argc, const char * argv[]) {

    tserver(argc,argv);

    return 0;

}

void tserver(int argc, const char * argv[]){

    std::cout << "t server" << std::endl;

    if(argc < 3){

        exit(-1);

    }

 

    const char* ip = argv[1];

    int port = atoi(argv[2]);

    int backlog = atoi(argv[3]);

 

    std::cout << "ip=" << ip << " port="<<port << " backlog=" << backlog  << std::endl;

 

    int fd;

    int check_ret;

 

    fd = socket(PF_INET,SOCK_DGRAM , 0);

    assert(fd >= 0);

 

    struct sockaddr_in addressServer;

    bzero(&addressServer,sizeof(addressServer));

 

    //轉換成網絡地址

    addressServer.sin_port = htons(port);

    addressServer.sin_family = AF_INET;

    //地址轉換

    inet_pton(AF_INET, ip, &addressServer.sin_addr);

    //發送數據

    const char* normal_data = "my boy!";

    if(sendto(fd, normal_data, strlen(normal_data),0,(struct sockaddr*)&addressServer,sizeof(addressServer)) < 0)

    {

      perror("Send File Name Failed:");

      exit(1);

    }

    close(fd);

}

4 select模型

select系統調用時用來讓我們的程序監視多個文件句柄的狀態變化的。程序會停在select這裏等待,直到被監視的文件句柄有一個或多個發生了狀態改變。

文件句柄,其實就是一個整數,表示一個系統資源,通過socket函數的聲明就明白了:

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

我們最熟悉的句柄是0、1、2三個,0是標準輸入,1是標準輸出,2是標準錯誤輸出。0、1、2是整數表示的,對應的FILE*結構的表示就是stdin、stdout、stderr。

繼續上面的select,就是用來監視某個或某些句柄的狀態變化的。select函數原型如下:

int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

函數的最後一個參數timeout是一個超時時間值。其類型是struct timeval *,即一個struct timeval結構的變量的指針,所以我們在程序裏要聲明一個struct timeval tv;然後把變量tv的地址&tv傳遞給select函數。struct timeval結構如下:

struct timeval

{

       long tv_sec;  //seconds

       long tv_usec; //microseconds

};

第2、3、4三個參數是一樣的類型fd_set *,即我們在程序裏要申請幾個fd_set類型的變量,比如rdfds,wtfds,exfds,然後把這個變量的地址&rdfds,&wtfds,&exfds傳遞給select函數。這三個參數都是一個句柄的集合,第一個rdfds是用來保存這樣的句柄的:當句柄的狀態變成可讀時系統就告訴select函數返回,同理第二個函數是指向有句柄狀態變成可寫時系統就會告訴select函數返回,同理第三個參數exfds是特殊情況,即句柄上有特殊情況發生時系統會告訴select函數返回。特殊情況比如對方通過一個socket句柄發來了緊急數據。如果我們程序裏只想檢測某個socket是否有數據可讀,我們可以這樣:

fd_set  rdfds;

struct timeval tv;

int ret;

FD_ZERO(&rdfds);

FD_SET(socket, &rdfds);

tv.tv_sec = 1;

tv.tv_uses = 500;

ret = select (socket + 1, &rdfds, NULL, NULL, &tv);

if(ret < 0) perror (“select”);

else if (ret==0) printf(“time out”);

else {

    printf(“ret = %d/n”,ret);

    if(FD_ISSET(socket, &rdfds)){

        /* 讀取socket句柄裏的數據 */

        recv(...);

    }

}

注意select函數的第一個參數,是所有加入集合的句柄值的最大那個那個值還要加1.比如我們創建了3個句柄;

int sa, sb, sc;

sa = socket(……);

connect (sa,….);

 

sb = socket(….);

connect (sb,…);

 

sc = socket(….);

connect(sc,…);

 

FD_SET(sa, &rdfds);

FD_SET(sb, &rdfds);

FD_SET(sc, &rdfds);

在使用select函數之前,一定要找到3個句柄中的最大值是哪個,我們一般定義一個變量來保存最大值,取得最大socket值如下:

int maxfd = 0;

if(sa > maxfd) maxfd = sa;

if(sb > maxfd) maxfd = sb;

if(sc > maxfd) maxfd = sc;

然後調用select函數:

ret = select (maxfd+1, &rdfds, NULL, NULL,&tv);

同樣的道理,如果我們是檢測用戶是否按了鍵盤進行輸入,我們就應該把標準輸入0這個句柄放到select裏來檢測,如下:

FD_ZERO(&rdfds);

FD_SET(0, &rdfds);

tv.tv_sec = 1;

tv.tv_usec = 0;

ret = select (1, &rdfds,NULL,NULL,&tv);

if(ret < 0) perror(“select”);

else if (ret = = 0) printf (“time out/n”);

else{

    scanf(“%s”,buf);

}

 實例<服務器>

使用select函數可以以非阻塞的方式和多個socket通信。程序只是演示select函數的使用,功能非常簡單,即使某個連接關閉以後也不會修改當前連接數,連接數達到最大值後會終止程序。

1. 程序使用了一個數組fd_A,通信開始後把需要通信的多個socket描述符都放入此數組。

2. 首先生成一個叫sock_fd的socket描述符,用於監聽端口。

3. 將sock_fd和數組fd_A中不爲0的描述符放入select將檢查的集合fdsr。

4. 處理fdsr中可以接收數據的連接。如果是sock_fd,表明有新連接加入,將新加入連接的socket描述符放置到fd_A。

select_server.cpp

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <errno.h>

#include <string.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

 

#define MYPORT 1234    // the port users will be connecting to

 

#define BACKLOG 5     // how many pending connections queue will hold

 

#define BUF_SIZE 200

 

int fd_A[BACKLOG];    // accepted connection fd

int conn_amount;    // current connection amount

 

void showclient()

{

    int i;

    printf("client amount: %d\n", conn_amount);

    for (i = 0; i < BACKLOG; i++) {

        printf("[%d]:%d  ", i, fd_A[i]);

    }

    printf("\n\n");

}

 

int main(void)

{

    int sock_fd, new_fd;  // listen on sock_fd, new connection on new_fd

    struct sockaddr_in server_addr;    // server address information

    struct sockaddr_in client_addr; // connector's address information

    socklen_t sin_size;

    int yes = 1;

    char buf[BUF_SIZE];

    int ret;

    int i;

 

    if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {

        perror("socket");

        exit(1);

    }

 

    if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) {

        perror("setsockopt");

        exit(1);

    }

   

    server_addr.sin_family = AF_INET;         // host byte order

    server_addr.sin_port = htons(MYPORT);     // short, network byte order

    server_addr.sin_addr.s_addr = INADDR_ANY; // automatically fill with my IP

    memset(server_addr.sin_zero, '\0', sizeof(server_addr.sin_zero));

 

    if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {

        perror("bind");

        exit(1);

    }

 

    if (listen(sock_fd, BACKLOG) == -1) {

        perror("listen");

        exit(1);

    }

 

    printf("listen port %d\n", MYPORT);

 

    fd_set fdsr;

    int maxsock;

    struct timeval tv;

 

    conn_amount = 0;

    sin_size = sizeof(client_addr);

    maxsock = sock_fd;

    while (1) {

        // initialize file descriptor set

        FD_ZERO(&fdsr);

        FD_SET(sock_fd, &fdsr);

 

        // timeout setting

        tv.tv_sec = 30;

        tv.tv_usec = 0;

 

        // add active connection to fd set

        for (i = 0; i < BACKLOG; i++) {

            if (fd_A[i] != 0) {

                FD_SET(fd_A[i], &fdsr);

            }

        }

 

        ret = select(maxsock + 1, &fdsr, NULL, NULL, &tv);

        if (ret < 0) {

            perror("select");

            break;

        } else if (ret == 0) {

            printf("timeout\n");

            continue;

        }

 

        // check every fd in the set

        for (i = 0; i < conn_amount; i++) {

            if (FD_ISSET(fd_A[i], &fdsr)) {

                ret = recv(fd_A[i], buf, sizeof(buf), 0);

                if (ret <= 0) {        // client close

                    printf("client[%d] close\n", i);

                    close(fd_A[i]);

                    FD_CLR(fd_A[i], &fdsr);

                    fd_A[i] = 0;

                } else {        // receive data

                    if (ret < BUF_SIZE)

                        memset(&buf[ret], '\0', 1);

                    printf("client[%d] send:%s\n", i, buf);

                }

            }

        }

 

        // check whether a new connection comes

        if (FD_ISSET(sock_fd, &fdsr)) {

            new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);

            if (new_fd <= 0) {

                perror("accept");

                continue;

            }

 

            // add to fd queue

            if (conn_amount < BACKLOG) {

                fd_A[conn_amount++] = new_fd;

                printf("new connection client[%d] %s:%d\n", conn_amount,

                        inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

                if (new_fd > maxsock)

                    maxsock = new_fd;

            }

            else {

                printf("max connections arrive, exit\n");

                send(new_fd, "bye", 4, 0);

                close(new_fd);

                break;

            }

        }

        showclient();

    }

 

    // close other connections

    for (i = 0; i < BACKLOG; i++) {

        if (fd_A[i] != 0) {

            close(fd_A[i]);

        }

    }

 

    exit(0);

}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章