理解 TCP(六):網絡編程接口

更好閱讀體驗:《理解 TCP 和 UDP》— By Gitbook javascript

一切皆 Socket

咱們已經知道網絡中的進程是經過 socket 來通訊的,那什麼是 socket 呢?
socket 起源於 UNIX,而 UNIX/Linux 基本哲學之一就是「一切皆文件」,均可以用「open → write/read → close」模式來操做。
socket 其實就是該模式的一個實現,socket 便是一種特殊的文件,一些 socket 函數就是對其進行的操做。 java

使用 TCP/IP 協議的應用程序一般採用系統提供的編程接口:UNIX BSD 的套接字接口(Socket Interfaces)
以此來實現網絡進程之間的通訊。
就目前而言,幾乎全部的應用程序都是採用 socket,因此說如今的網絡時代,網絡中進程通訊是無處不在,一切皆 socket c++

套接字接口 Socket Interfaces

套接字接口是一組函數,由操做系統提供,用以建立網絡應用。
大多數現代操做系統都實現了套接字接口,包括全部 Unix 變種,Windows 和 Macintosh 系統。 git

套接字接口的起源
套接字接口是加州大學伯克利分校的研究人員在 20 世紀 80 年代早起提出的。
伯克利的研究者使得套接字接口適用於任何底層的協議,第一個實現就是針對 TCP/IP 協議,他們把它包括在 Unix 4.2 BSD 的內核裏,而且分發給許多學校和實驗室。
這在因特網的歷史成爲了一個重大事件。
—— 《深刻理解計算機系統》編程

從 Linux 內核的角度來看,一個套接字就是通訊的一個端點。
從 Linux 程序的角度來看,套接字是一個有相應描述符的文件。
普通文件的打開操做返回一個文件描述字,而 socket() 用於建立一個 socket 描述符,惟一標識一個 socket。
這個 socket 描述字跟文件描述字同樣,後續的操做都有用到它,把它做爲參數,經過它來進行一些操做。 服務器

經常使用的函數有:網絡

  • socket()
  • bind()
  • listen()
  • connect()
  • accept()
  • write()
  • read()
  • close()

Socket 的交互流程

socket 交互過程.png

圖中展現了 TCP 協議的 socket 交互流程,描述以下:數據結構

  1. 服務器根據地址類型、socket 類型、以及協議來建立 socket。
  2. 服務器爲 socket 綁定 IP 地址和端口號。
  3. 服務器 socket 監聽端口號請求,隨時準備接收客戶端發來的鏈接,這時候服務器的 socket 並無所有打開。
  4. 客戶端建立 socket。
  5. 客戶端打開 socket,根據服務器 IP 地址和端口號試圖鏈接服務器 socket。
  6. 服務器 socket 接收到客戶端 socket 請求,被動打開,開始接收客戶端請求,知道客戶端返回鏈接信息。這時候 socket 進入阻塞狀態,阻塞是因爲 accept() 方法會一直等到客戶端返回鏈接信息後才返回,而後開始鏈接下一個客戶端的鏈接請求。
  7. 客戶端鏈接成功,向服務器發送鏈接狀態信息。
  8. 服務器 accept() 方法返回,鏈接成功。
  9. 服務器和客戶端經過網絡 I/O 函數進行數據的傳輸。
  10. 客戶端關閉 socket。
  11. 服務器關閉 socket。

這個過程當中,服務器和客戶端創建鏈接的部分,就體現了 TCP 三次握手的原理。 dom

下面詳細講一下 socket 的各函數。 socket

Socket 接口

socket 是系統提供的接口,而操做系統大多數都是用 C/C++ 開發的,天然函數庫也是 C/C++ 代碼。

socket 函數

該函數會返回一個套接字描述符(socket descriptor),可是該描述符僅是部分打開的,還不能用於讀寫。
如何完成打開套接字的工做,取決於咱們是客戶端仍是服務器。

函數原型

#include <sys/socket.h>

int socket(int domain, int type, int protocol);複製代碼

參數說明

domain:
協議域,決定了 socket 的地質類型,在通訊中必須採用對應的地址。
經常使用的協議族有:AF_INET(ipv4地址與端口號的組合)、AF_INET6(ipv6地址與端口號的組合)、AF_LOCAL(絕對路徑名做爲地址)。
該值的常量定義在 sys/socket.h 文件中。

type:
指定 socket 類型。
經常使用的類型有:SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等。
其中 SOCK_STREAM 表示提供面向鏈接的穩定數據傳輸,即 TCP 協議。
該值的常量定義在 sys/socket.h 文件中。

protocol:
指定協議。
經常使用的協議有:IPPROTO_TCP(TCP協議)、IPPTOTO_UDP(UDP協議)、IPPROTO_SCTP(STCP協議)。
當值位 0 時,會自動選擇 type 類型對應的默認協議。

bind 函數

由服務端調用,把一個地址族中的特定地址和 socket 聯繫起來。

函數原型

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);複製代碼

參數說明

sockfd:
即 socket 描述字,由 socket() 函數建立。

*addr
一個 const struct sockaddr 指針,指向要綁定給 sockfd 的協議地址。
這個地址結構根據地址建立 socket 時的地址協議族不一樣而不一樣,例如 ipv4 對應 sockaddr_in,ipv6 對應 sockaddr_in6.
這幾個結構體在使用的時候,均可以強制轉換成 sockaddr
下面是這幾個結構體對應的所在的頭文件:

  1. sockaddrsys/socket.h
  2. sockaddr_innetinet/in.h
  3. sockaddr_in6netinet6/in.h

_in 後綴意義:互聯網絡(internet)的縮寫,而不是輸入(input)的縮寫。

listen 函數

服務器調用,將 socket 從一個主動套接字轉化爲一個監聽套接字(listening socket), 該套接字能夠接收來自客戶端的鏈接請求。
在默認狀況下,操做系統內核會認爲 socket 函數建立的描述符對應於主動套接字(active socket)。

函數原型

#include <sys/socket.h>
int listen(int sockfd, int backlog);複製代碼

參數說明

sockfd:
即 socket 描述字,由 socket() 函數建立。

backlog:
指定在請求隊列中的最大請求數,進入的鏈接請求將在隊列中等待 accept() 它們。

connect 函數

由客戶端調用,與目的服務器的套接字創建一個鏈接。

函數原型

#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);複製代碼

參數說明

clientfd:
目的服務器的 socket 描述符

*addr:
一個 const struct sockaddr 指針,包含了目的服務器 IP 和端口。

addrlen
協議地址的長度,若是是 ipv4 的 TCP 鏈接,通常爲 sizeof(sockaddr_in);

accept 函數

服務器調用,等待來自客戶端的鏈接請求。
當客戶端鏈接,accept 函數會在 addr 中會填充上客戶端的套接字地址,而且返回一個已鏈接描述符(connected descriptor),這個描述符能夠用來利用 Unix I/O 函數與客戶端通訊。

函數原型

#indclude <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);複製代碼

參數說明

listenfd:
服務器的 socket 描述字,由 socket() 函數建立。

*addr:
一個 const struct sockaddr 指針,用來存放提出鏈接請求客戶端的主機的信息

*addrlen:
協議地址的長度,若是是 ipv4 的 TCP 鏈接,通常爲 sizeof(sockaddr_in)

close 函數

在數據傳輸完成以後,手動關閉鏈接。

函數原型

#include <sys/socket.h>
#include <unistd.h>
int close(int fd);複製代碼

參數說明

fd:
須要關閉的鏈接 socket 描述符

網絡 I/O 函數

當客戶端和服務器創建鏈接後,可使用網絡 I/O 進行讀寫操做。
網絡 I/O 操做有下面幾組:

  1. read()/write()
  2. recv()/send()
  3. readv()/writev()
  4. recvmsg()/sendmsg()
  5. recvfrom()/sendto()

最經常使用的是 read()/write()
他們的原型是:

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);複製代碼

鑑於該文是側重於描述 socket 的工做原理,就再也不詳細描述這些函數了。

實現一個簡單 TCP 交互

服務端

// socket_server.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define MAXLINE 4096 // 4 * 1024

int main(int argc, char **argv) {
    int listenfd, // 監聽端口的 socket 描述符
        connfd;   // 鏈接端 socket 描述符
    struct sockaddr_in servaddr;
    char buff[MAXLINE];
    int n;

    // 建立 socket,而且進行錯誤處理
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    // 初始化 sockaddr_in 數據結構
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(6666);

    // 綁定 socket 和 端口
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
    {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    // 監聽鏈接
    if (listen(listenfd, 10) == -1)
    {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    printf("====== Waiting for client's request======\n");

    // 持續接收客戶端的鏈接請求
    while (true)
    {
        if ((connfd = accept(listenfd, (struct sockaddr *)NULL, NULL) == -1))
        {
            printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
            continue;
        }

        n = recv(connfd, buff, MAXLINE, 0);
        buff[n] = '\0';
        printf("recv msg from client: %s\n", buff);
        close(connfd);
    }

    close(listenfd);
    return 0;
}複製代碼

客戶端

// socket_client.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define MAXLINE 4096

int main(int argc, char **argv) {
    int sockfd, n;
    char recvline[4096], sendline[4096];
    struct sockaddr_in servaddr;

    if (argc != 2)
    {
        printf("usage: ./client <ipaddress>\n");
        return 0;
    }

    // 建立 socket 描述符
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    // 初始化目標服務器數據結構
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    // 從參數中讀取 IP 地址
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    {
        printf("inet_pton error for %s\n", argv[1]);
        return 0;
    }

    // 鏈接目標服務器,並和 sockfd 聯繫起來。
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        printf("connect error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    printf("send msg to server: \n");

    // 從標準輸入流中讀取信息
    fgets(sendline, 4096, stdin);

    // 經過 sockfd,向目標服務器發送信息
    if (send(sockfd, sendline, strlen(sendline), 0) < 0)
    {
        printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }

    // 數據傳輸完畢,關閉 socket 鏈接
    close(sockfd);
    return 0;
}複製代碼

Run

首先建立 makefile 文件

all:server client
server:socket_server.o
    g++ -g -o socket_server socket_server.o
client:socket_client.o
    g++ -g -o socket_client socket_client.o
socket_server.o:socket_server.cpp
    g++ -g -c socket_server.cpp
socket_client.o:socket_client.cpp
    g++ -g -c socket_client.cpp
clean:all
    rm all複製代碼

而後使用命令:

$ make複製代碼

會生成兩個可執行文件:

  1. socket_server
  2. socket_client

分別打開兩個終端,運行:

  1. ./socket_server
  2. ./socket_client 127.0.0.1

而後在 socket_client 中鍵入發送內容,能夠再 socket_server 接收到一樣的信息。

參考

《後臺開發 核心技術與應用實踐》
《計算機網絡》

相關文章
相關標籤/搜索