快速理解 socket 編程 (C/C++)

參考
    http://c.biancheng.net/cpp/socket/

本教程不要求讀者有Linux和Windows開發(fā)經(jīng)驗, 也不需要深入了解 TCP/IP 協(xié)議, 涉及到相關知識時都會說明

同時學 Linux和Windows 的原因

大多項目在 Windows/Linux 下開發(fā) client/server 端, 單獨學1種平臺 沒實踐意義

兩大平臺下 socket 編程非常相似

網(wǎng)絡編程 就是 編寫程序 使兩臺聯(lián)網(wǎng)的計算機 相互交換數(shù)據(jù), 這就是 socket 全部內(nèi)容 嗎?是的!

socket 編程 遠比想象中簡單

chapter1 socket 簡介

socket: 套接字, 計算機間通信的一種 約定

socket 典型應用: Web 服務器 和 瀏覽器

    瀏覽器     
        獲取 用戶輸入的 URL
        向 服務器 發(fā)起請求

    服務器
        分析收到的 URL
        將對應的網(wǎng)頁內(nèi)容返回給瀏覽器
        
    瀏覽器
        解析和渲染, 將文字、圖片、視頻 呈現(xiàn)給用戶

1 IP Address

(1) 封裝 到要發(fā)送的數(shù)據(jù)包

(2) 被 路由器 用于 尋址: 據(jù) IP Address 找到 dst 計算機

本機地址: 127.0.0.1

2 Port

(1) 用于 區(qū)分 不同的 網(wǎng)絡程序

    網(wǎng)絡程序    端口號
    Web 服務  80
    FTP 服務  21
    SMTP 服務     25  

(2) 是 虛擬/邏輯 概念

可視為 一道門, data 通過這道門 流入流出, 每道門有不同的 編號, 即 端口號

3 Protocol

網(wǎng)絡通信的雙方遵守的約定

TCP/IP 協(xié)議族: TCP IP UDP Telnet FTP SMTP 等上百個關聯(lián)協(xié)議
    TCP IP 常用

4 數(shù)據(jù)傳輸方式

常用2種:

(1) SOCK_STREAM

    面向連接
        重發(fā)
    
    http 協(xié)議用

(2) SOCK_DGRAM

    無連接
        不作數(shù)據(jù)校驗
        錯了不重發(fā)
        
    QQ 視頻/語音聊天 用

總結

IP Address 和 Port 能在互聯(lián)網(wǎng)中 定位到要通信的程序

Protocol 和 數(shù)據(jù)傳輸方式 規(guī)定了 如何傳輸 數(shù)據(jù)

有了這些, 兩臺計算機就可以通信了

chapter2 Linux 下 socket 程序 Demo

功能: client 從 server 讀1個字符串, 并打印出來

Linux 中, socket 也是文件, 有文件描述符, 可用 write() / read() 進行 I/O

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

int main()
{
    // [1] 建 套接字:  IPv4 地址 / 面向連接的傳輸方式 / TCP 協(xié)議
    int listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    
    // Note: 指定 本端(Server) 協(xié)議族 / port / ipaddr
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr) );  
    serv_addr.sin_family = AF_INET;  
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 具體的IP地址
    serv_addr.sin_port = htons(1234);  // 端口
    
    // [2] bind 套接字 和 IPAddr/Port
    bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr) );
    
    printf("listening:...\n");
    // [3] 監(jiān)聽: 將 `普通套接字` 轉化為 `監(jiān)聽套接字`
    listen(listenfd, 20); // connnection queue 最大 size

    struct sockaddr_in clientAddr;
    socklen_t clientAddrSize = sizeof(clientAddr);
    printf("accepting:...\n");
    
    // [4] 接收 client request: 
    // 1] 阻塞 thread, 直到 client connection 到達 + 被 OS kernel 接受
    // 2] 3次握手: 建立連接
    // 3] 返回 `已連接套接字`
    int connfd = accept(listenfd, (struct sockaddr*)&clientAddr, &clientAddrSize);
    
    // [5] 發(fā) 數(shù)據(jù) 給 client: 向 套接字文件 寫 數(shù)據(jù) 
    char str[] = "Hello World!";
    write(connfd, str, sizeof(str) );
   
    // [6] 關 套接字
    close(connfd);
    close(listenfd);
}
// client.cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main()
{
    // [1] 建 套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // Note: 指定 對端(Server) 協(xié)議族 / port / ipaddr
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  
    serv_addr.sin_family = AF_INET; 
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); 
    serv_addr.sin_port = htons(1234);  
    
    // [2] request to server, 直到 server 傳回數(shù)據(jù)后, connect() 才 return
    connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr) );
   
    // [3] 讀 對端的 Response 
    char buffer[40];
    read(sockfd, buffer, sizeof(buffer)-1 );
   
    printf("Message form server: %s\n", buffer);
   
    // [4] 關 套接字
    close(sockfd);
}

Note: server 只接受1次 client 請求, server 向 client 傳回數(shù)據(jù)后, 程序運行結束

(1) 在1個終端 編譯 -> 運行 server -> accept() 阻塞

    $ g++ server.cpp -o server
    $ ./server
    listening:...
    accepting:...

(2) 在1個終端 編譯 -> 運行 client

    $ g++ client.cpp -o client
    $ ./client
    Message form server: Hello World!

chapter3 Windows 下 socket 程序 Demo

server.cpp / client.cpp 分別編譯 為 server.exe / client.exe

    先運行 server.exe
        >server.exe
        listening:...
        accepting:...

    再運行 client.exe
        >client.exe
        Message form server: Hello World!

linux/Windows 下 主要區(qū)別:

(1) Windows 下 socket 程序依賴 動態(tài)鏈接庫 Winsock.dll 或 ws2_32.dll,必須提前加載

(2) Linux 用 "文件描述符", Windows 用 "文件句柄"

Linux 不區(qū)分 socket 文件和普通文件, 而 Windows 區(qū)分

    Linux/Windows 下 socket() 返回 int/SOCKET 型(句柄)
// server.cpp
#include <stdio.h>
#include <winsock2.h>

#pragma comment (lib, "ws2_32.lib")  // load ws2_32.dll

int main()
{
    // init DLL
    WSADATA wsaData;
    WSAStartup( MAKEWORD(2, 2), &wsaData);
    
    // (1)
    SOCKET listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    
    sockaddr_in sockAddr;
    memset(&sockAddr, 0, sizeof(sockAddr)); 
    sockAddr.sin_family = PF_INET;
    sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");  
    sockAddr.sin_port = htons(1234);  
    
    // (2)
    bind(listenfd, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

    printf("listening:...\n");
    // (3)
    listen(listenfd, 20);
    
    SOCKADDR clientAddr;
    int clientAddrSize = sizeof(SOCKADDR);
    
    printf("accepting:...\n");
    // (4)
    SOCKET connfd = accept(listenfd, (SOCKADDR*)&clientAddr, &clientAddrSize);

    const char *str = "Hello World!";
    
    // (5)
    send(connfd, str, strlen(str)+sizeof(char), NULL);
    
    // (6) 
    closesocket(connfd);
    closesocket(listenfd);
    
    // terminate DLL
    WSACleanup();
}
// client.cpp
// client.cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main()
{
    // [1] 建 套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // Note: 指定 對端(Server) 協(xié)議族 / port / ipaddr
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  
    serv_addr.sin_family = AF_INET; 
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); 
    serv_addr.sin_port = htons(1234);  
    
    // [2] request to server, 直到 server 傳回數(shù)據(jù)后, connect() 才 return
    connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr) );
   
    // [3] 讀 對端的 Response 
    char buffer[40];
    read(sockfd, buffer, sizeof(buffer)-1 );
   
    printf("Message form server: %s\n", buffer);
   
    // [4] 關 套接字
    close(sockfd);
}

chapter4 socket()

1 文件

Linux 中, 一切都是文件: 極大地簡化了程序員的理解和操作, 使得對 硬件設備 的處理 像 普通文件一樣

    文本文件
    
    源文件
    
    二進制文件
    
    硬件設備 可被 `映射` 為 虛擬文件/設備文件
        鍵盤  <-> stdin 標準輸入文件
        顯示器 <-> stdout 標準輸出文件
    
    socket

所有 文件 都可用 read()/write() 讀/寫數(shù)據(jù)

創(chuàng)建的文件都有1個 int 型編號, 即 文件描述符(File Descriptor) - Windows 下稱 文件句柄*File Handle)

使用文件 時, 只要知道 文件描述符 即可

    stdin  描述符 0
    stdout 描述符 1

2臺計算機間 1次 socket 通信, 實際上是 server/client 對 1個 socket 文件 的 1次 寫/讀

2 Linux 建 socket

    <sys/socket.h> 

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

af: 地址族(Address Family)

    AF_INET / AF_INET6 (IPv4 / IPv6 地址)

type 數(shù)據(jù)傳輸方式

    SOCK_STREAM / SOCK_DGRAM

protocol 傳輸協(xié)議

    IPPROTO_TCP / IPPTOTO_UDP

IPv4 地址 + SOCK_STREAM/SOCK_DGRAM 傳輸 => OS 可自動推導出 傳輸協(xié)議(只能是) TCP/UDP

=>

    // TCP 套接字
    int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  

    // UDP 套接字
    int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); 


    可將 protocol 的值設為 0, 讓 OS 自動推導出 傳輸協(xié)議
    int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //創(chuàng)建TCP套接字
    int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //創(chuàng)建UDP套接字

chapter5 bind() / connect()

bind()

server: 綁定 套接字與 server IPAddr/Port
    int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen);
    
        addrlen 可用 sizeof() 求

定義為 sockaddr_in 型, bind() 中 強轉為 sockaddr

sockaddr 是 sockaddr_in(IPv4) 的1層 封裝, 以 同時支持 sockaddr_in6(Ipv6)

    第1字段相同
    其余部分 是 char[14]
    // Note: 指定 本端(Server) 協(xié)議族 / port / ipaddr
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr) );  
    serv_addr.sin_family = AF_INET;  
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  
    serv_addr.sin_port = htons(1234);  
    
    // [2] bind 套接字 和 IPAddr/Port
    bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr) );

in_addr_t 定義在頭文件 <netinet/in.h>, 等價于 unsigned long,長 4 Byte
s_addr 是整數(shù), 而 用戶輸入的 IPAddr 是字符串, 用要 inet_addr() 轉換

struct sockaddr_in
{
    sa_family_t     sin_family;   // Address Family
    uint16_t        sin_port;     // 16位 Port       = 2 Byte 
    struct in_addr  sin_addr;     // 32位 IPAddr     = 4 Byte
    char            sin_zero[8];  // 不用, 一般填 0 = 8 Byte
};

struct in_addr{
    in_addr_t  s_addr;  // 32位 IPAddr
};

    sockaddr_in
     ——————————————
    | sin_family   | 
    |              |
    | uint16_t     |          in_addr_t
    |              |         —— —— —— ——
    | sin_addr     | - - -> | s_addr    |
    |              |         —— —— —— ——
    | sin_zero[8]  | 
    |              |
     ——————————————

    Port: uint16_t 長 2Byte, 取值范圍 0~65536, 
        但 0~1023 端口一般由系統(tǒng)分配給特定的服務程序
            Web 服務 Port 80
            FTP 服務 Port 21
        client 程序 要盡量用 Port 1024~65536 
        
struct sockaddr
{
    sa_family_t  sin_family;   
    char         sa_data[14];  // Port + IPAddr + 填充 
};
        

connect(): 與 bind() 原型相同

區(qū)別: bind()/connect() 是 server/client 端 用來 綁定/連接到 本端(server)/對端(server)

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

chapter6 listen() accept()

server 端

(1) bind() 綁定 套接字

(2) listen() 讓 套接字 進入 (被動)監(jiān)聽狀態(tài)

(3) accept() 可 隨時響應 client 端的 請求

listen()

int listen(int sockfd, int connQueMaxSize)
 
    sockfd: 想進入 監(jiān)聽狀態(tài)的 `普通套接字`
    
    connQueMaxSize: connnection queue 最大 size

被動監(jiān)聽 態(tài): 沒 client 端 請求時, 套接字處于 sleep 狀態(tài), 直到收到 client 端請求, 套接字才被 "喚醒" 來 Respond

Note: listen() 讓套接字進入監(jiān)聽狀態(tài), 不阻塞; accept() 阻塞, until 有新 Request 到來

請求隊列: Request Queue / connnection queue

    server 端 套接字 正處理某個 client 的 Request 時, 
        又收到 other clients 的 Requests, 放 Request Queue/buffer
    待 `當前 Request` 處理完畢后, 再從 Request Queue 取出處理

accept()

    int accept(int listenfd, struct sockaddr *clientAddr, socklen_t *pClientAddrLen);

listenfd 是 server 端 套接字

clientAddr 保存 client IPAddr/Port

accept() 返回 已連接套接字 connfd 來和 client 端 通信

chapter7 socket 數(shù)據(jù)的 發(fā)送和接收

Linux下 socket 數(shù)據(jù)的 發(fā)送和接收

Linux 中, 一切都是文件

所有 文件(如 socket ) 都可用 read()/write() 讀/寫數(shù)據(jù)

2臺計算機間 1次 socket 通信, 實際上是 server/client 對 1個 socket 文件 的 1次 寫/讀

chapter8 TCP 3次握手 & 4次揮手

1 3次握手 / 4次揮手 本質

C與S 兩端 都 進行一次 發(fā) SYN/FIN + 收 ACK, 來 確保 自己發(fā)的 SYN/FIN 被對方收到

            => 都是 4 個 分節(jié): 1 / 2 / 3 / 4

區(qū)別

1] 連接建立 第2/3 分節(jié) 合并 為1個分節(jié) 都由 對端發(fā)送 => 3次 握手

2] 連接終止 第2/3 分節(jié) 不能合并 為1個分節(jié) => 4次 揮手

原因: dataRecvQueue 還有 data, FIN 還沒取到

2 SYN / FIN & ACK 分節(jié)序號

SYN 與 FIN 均 占 1 Byte 序號空間 =>

    1) SYN / FIN 序號 x: 本端本次想要發(fā)的序號 
    
    2) ACK 序號 y      : `期待接收` 的 `對端下一(次) 序號` = x+1
觀察分組
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 網(wǎng)絡中進程之間如何通信 為了方便大家獲取源代碼,可以移步這里,GitHub源代碼 進程通信的概念最初來源于單機系統(tǒng)...
    batbattle閱讀 14,253評論 1 5
  • 下面為Daytime這個服務的源代碼例子,同時兼容IPV6和IPV4的地址,最后部分有更多說明。 單播模式下的Se...
    天楚銳齒閱讀 6,045評論 0 2
  • 什么是socket(套接字) socket是一種計算機間約定好的傳輸方式(有點抽象)。其本身為一串數(shù)字,unix將...
    Cooder閱讀 334評論 0 0
  • 簡介 Socket理論 Socket工作流程 核心函數(shù)講解 服務的如何獲取客戶端的信息 字符串ip和網(wǎng)絡二進制的轉...
    第八區(qū)閱讀 4,107評論 0 4
  • 網(wǎng)絡編程離不開socket,小猿圈這篇詳解一下socket創(chuàng)建,仔細學完這篇對你認識網(wǎng)絡底層的東西有著很重要的作用...
    小猿圈加加閱讀 155評論 0 0

友情鏈接更多精彩內(nèi)容