(四十九)socket編程——網絡套接字函數及建立C/S模型(TCP)

一、網絡套接字函數

1)socket

#include <sys/types.h>
#include <sys/socket.h>

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


domain:
    AF_INET 這是大多數用來產生socket的協議,使用TCP或UDP來傳輸,用IPv4的地址
    AF_INET6 與上面類似,不過是來用IPv6的地址
    AF_UNIX 本地協議,使用在Unix和Linux系統上,一般都是當客戶端和服務器在同一臺及其上的時候使用

type:
    SOCK_STREAM 這個協議是按照順序的、可靠的、數據完整的基於字節流的連接。這是一個使用最多的socket類型,這個socket是使用TCP來進行傳輸。
    SOCK_DGRAM 這個協議是無連接的、固定長度的傳輸調用。該協議是不可靠的,使用UDP來進行它的連接。
    SOCK_SEQPACKET 這個協議是雙線路的、可靠的連接,發送固定長度的數據包進行傳輸。必須把這個包完整的接受才能進行讀取。
    SOCK_RAW 這個socket類型提供單一的網絡訪問,這個socket類型使用ICMP公共協議。(ping、traceroute使用該協議)
    SOCK_RDM 這個類型是很少使用的,在大部分的操作系統上沒有實現,它是提供給數據鏈路層使用,不保證數據包的順序

protocol:
    一般填0 默認協議

返回值:
    成功返回一個新的文件描述符,失敗返回-1,設置errno

  socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣返回一個文件描述符,應用程序可以像讀寫文件一樣用read/write在網絡上收發數據,如果socket()調用出錯則返回-1。對於IPv4,domain參數指定爲AF_INET。對於TCP協議,type參數指定爲SOCK_STREAM,表示面向流的傳輸協議。如果是UDP協議,則type參數指定爲SOCK_DGRAM,表示面向數據報的傳輸協議。protocol參數的一般指定爲0即可。
  該函數更詳細的介紹請參考另外的博客http://blog.csdn.net/liuxingen/article/details/44995467
  
  
  

2)bind

#include <sys/types.h>
#include <sys/socket.h>

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

sockfd:
    socket文件描述符
addr:
    構造出IP地址加端口號
addrlen:
    sizeof(addr)長度

返回值:
    成功返回0,失敗返回-1, 設置errno

  服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號後就可以向服務器發起連接,因此服務器需要調用bind綁定一個固定的網絡地址和端口號
  bind()的作用是將參數sockfd和addr綁定在一起,使sockfd這個用於網絡通訊的文件描述符監聽addr所描述的地址和端口號。前面講過,struct sockaddr *是一個通用指針類型,addr參數實際上可以接受多種協議的sockaddr結構體,而它們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度。如:

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));     // 清0操作
servaddr.sin_family = AF_INET;          // 指定爲IPv4類型的地址
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 本地的任意IP地址
servaddr.sin_port = htons(8000);    // 端口號爲8000

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

3)listen

#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);

sockfd:
    socket文件描述符
backlog:
    排隊建立3次握手隊列和剛剛建立3次握手隊列的鏈接數和(一般填128

  查看系統默認backlog值

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

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

4)accept

#include <sys/types.h>
#include <sys/socket.h>

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

sockdf:
    socket文件描述符
addr:
    傳出參數,返回鏈接客戶端地址信息,含IP地址和端口號
addrlen:
    傳入傳出參數(值-結果),傳入sizeof(addr)大小,函數返回時返回真正接收到地址結構體的大小

返回值:
    成功返回一個新的socket文件描述符,用於和客戶端通信,失敗返回-1,設置errno

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

while (1) {
    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
    n = read(connfd, buf, MAXLINE);   // 注意到這裏的connfd是accept的返回參數
    ......
    close(connfd);
}

  整個是一個while死循環,每次循環處理一個客戶端連接(如果有客戶請求)。由於cliaddr_len是傳入傳出參數,每次調用accept()之前應該重新賦初值。accept()的參數listenfd是先前的監聽文件描述符,而accept()的返回值是另外一個文件描述符connfd,之後與客戶端之間就通過這個connfd通訊,最後關閉connfd斷開連接,而不關閉listenfd,再次回到循環開頭listenfd仍然用作accept的參數。accept()成功返回一個文件描述符,出錯返回-1。
  
  
  

5)connect

#include <sys/types.h>
#include <sys/socket.h>

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

sockdf:
    socket文件描述符
addr:
    傳入參數,指定服務器端地址信息,含IP地址和端口號
addrlen:
    傳入參數,傳入sizeof(addr)大小

返回值:
    成功返回0,失敗返回-1,設置errno

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


二、C/S模型-TCP

  下圖是基於TCP協議的客戶端/服務器程序的一般流程:
  這裏寫圖片描述
  服務器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處於監聽端口的狀態,客戶端調用socket()初始化後,調用connect()發出SYN段並阻塞等待服務器應答,服務器應答一個SYN-ACK段,客戶端收到後從connect()返回,同時應答一個ACK段,服務器收到後從accept()返回。
  數據傳輸的過程:
  建立連接後,TCP協議提供全雙工的通信服務,但是一般的客戶端/服務器程序的流程是由客戶端主動發起請求,服務器被動處理請求,一問一答的方式。因此,服務器從accept()返回後立刻調用read(),讀socket就像讀管道一樣,如果沒有數據到達就阻塞等待,這時客戶端調用write()發送請求給服務器,服務器收到後從read()返回,對客戶端的請求進行處理,在此期間客戶端調用read()阻塞等待服務器的應答,服務器調用write()將處理結果發回給客戶端,再次調用read()阻塞等待下一條請求,客戶端收到後從read()返回,發送下一條請求,如此循環下去。
  如果客戶端沒有更多的請求了,就調用close()關閉連接,就像寫端關閉的管道一樣,服務器的read()返回0,這樣服務器就知道客戶端關閉了連接,也調用close()關閉連接。注意,任何一方調用close()後,連接的兩個傳輸方向都關閉,不能再發送數據了。如果一方調用shutdown()則連接處於半關閉狀態,仍可接收對方發來的數據。
  
  
  
  模型如下:

/* server.c */
/* 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.構造用於TCP通信的套接字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);
        //輸出客戶端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 */
/* 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[] = "192.168.6.254";
    char buf[MAXLINE];
    if (argc < 2) {
        printf("./client serverIP str\n");
        exit(1);
    }
    //1.創建一個socket
    confd = socket(AF_INET, SOCK_STREAM, 0);
    //2.初始化服務器地址
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    //字符串argv[1]
    inet_pton(AF_INET, argv[1], &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port  = htons(SERVER_PORT);
    //3.鏈接服務器
    connect(confd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    //4.請求服務器處理數據
    write(confd, argv[2], strlen(argv[2]));
    len = read(confd, buf, sizeof(buf));
    buf[len] = '\n';
    write(STDOUT_FILENO, buf, len + 1);

    //5.關閉socket
    close(confd);
    return 0;
}

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

發佈了73 篇原創文章 · 獲贊 17 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章