{"id":24991912,"url":"https://github.com/davidleitw/socket","last_synced_at":"2025-04-12T02:13:33.140Z","repository":{"id":46039076,"uuid":"419060379","full_name":"davidleitw/socket","owner":"davidleitw","description":"簡單的 socket programming 入門筆記。","archived":false,"fork":false,"pushed_at":"2023-02-04T16:28:32.000Z","size":79,"stargazers_count":111,"open_issues_count":0,"forks_count":10,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-12T02:13:27.663Z","etag":null,"topics":["c","linux","network","socket","socket-programming","tcp","tutorial","tutorial-code","tutorials","udp"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/davidleitw.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-10-19T19:12:57.000Z","updated_at":"2025-04-07T04:54:38.000Z","dependencies_parsed_at":"2022-09-02T14:02:34.074Z","dependency_job_id":null,"html_url":"https://github.com/davidleitw/socket","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidleitw%2Fsocket","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidleitw%2Fsocket/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidleitw%2Fsocket/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidleitw%2Fsocket/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/davidleitw","download_url":"https://codeload.github.com/davidleitw/socket/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248505925,"owners_count":21115354,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c","linux","network","socket","socket-programming","tcp","tutorial","tutorial-code","tutorials","udp"],"created_at":"2025-02-04T13:52:55.852Z","updated_at":"2025-04-12T02:13:33.111Z","avatar_url":"https://github.com/davidleitw.png","language":"C","readme":"# socket programming\n\n`socket` 本質上是一種 **IPC** (`Inter-Process Communication`) 的技術，用於兩個或多個 `process` 進行資料交換或者通訊。\n\n在網路領域，`socket` 著重的不是同一台主機間 `process` 的通訊，而是不同主機執行的 `process` 互相交換資料的通訊。\n\n我們在寫 `socket programming` 的時候會使用 `os` 提供的 `API`，來避免重複造輪子，今天的筆記會簡單介紹一下 `linux` 提供的 `socket API`，並用兩個簡單的範例介紹如何用 `tcp` 跟 `udp` 協定透過 `socket` 傳輸資料。\n\n![](https://i.imgur.com/gXp0tLh.png)\n\n本文章所使用的環境\n\n- ***kernel***: `5.11.0-37-generic`\n- ***gcc version***: `gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0`\n- ***GNU Make***: `4.2.1`\n\n在寫 `socket` 相關的程式的時候，需要先\n\n```c\n#include \u003carpa/inet.h\u003e  // sockaddr 相關\n#include \u003csys/socket.h\u003e\n```\n\n## socket\n\n```c\nint socket(int domain, int type, int protocol)\n```\n\n#### *domain*\n定義要建立哪一種類型的 `socket`，常用的有以下幾種類型\n- **AF_UNIX**, **AF_LOCAL**: 用於本機間 `process` 的溝通   \n- **AF_INET**, **AF_INET6**\n    - **AF_INET**: IPv4 協定\n    - **AF_INET6**: IPv6 協定\n\n詳細的選項可以參考 `socket` 的 [man page](https://man7.org/linux/man-pages/man2/socket.2.html)\n\n#### *type*\n`socket` 傳輸資料的手段(`communication semantics`)\n\n- **SOCK_STREAM**: 對應到 `tcp` 協定\n- **SOCK_DGRAM**: 對應到 `udp` 協定\n\n#### *protocol*\n設定通訊協定的號碼，通常在寫的時候會填入 `0`，`kernel` 會根據上面的兩個參數自動選擇合適的協定。\n\n- [protocol man page](https://man7.org/linux/man-pages/man5/protocols.5.html#top_of_page)\n\n`/etc/protocols` 可以看到 `linux` 底下支援的協定\n\n#### *Return Value*\n\n成功建立 `socket` 之後，此函式會返回該 `socket` 的**檔案描述符**(`socket file descriptor`)，在之後的操作可以透過這個回傳值來操作我們建立的 `socket`。 如果建立失敗則會回傳 `-1(INVALID_SOCKET)`\n\n### 檔案描述符是什麼?\n\n\n參考資料\n- [Everything is a file](https://en.wikipedia.org/wiki/Everything_is_a_file)\n- [Linux 的 file descriptor 筆記 FD 真的好重要](https://kkc.github.io/2020/08/22/file-descriptor/)\n- [Linux 下 socket 通訊所用的 sockfd 怎麼來的](https://www.cnblogs.com/chorm590/p/12745824.html)\n\n### 建立 socket example\n\n```c\n#include \u003cstdio.h\u003e\n#include \u003csys/socket.h\u003e\n\nint main() {\n    // AF_INET = IPv4\n    // SOCK_DGRAM = UDP\n    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);\n    \n    // 檢查是否建立成功\n    if (socket_fd \u003c 0) {\n        printf(\"Fail to create a socket.\");\n    }\n    \n    // 根據 socker_fd 關閉剛剛創立的 socket\n    close(socket_fd);\n    return 0;\n}\n```\n\n接著先來介紹一下 `socket` 中拿來儲存地址的資料結構 **`sockaddr`**\n\n## sockaddr\n\n`sockaddr` 是 `socket` 的通用地址結構，就如同一開始提到的，`socket` 除了在網路領域之外，也可以在很多不同的地方用來通訊。\n\n`sockaddr` 結構，定義如下\n\n```c\ntypedef unsigned short int sa_family_t;\n\n#define\t__SOCKADDR_COMMON(sa_prefix) \\\n  sa_family_t sa_prefix##family\n\nstruct sockaddr {\n    __SOCKADDR_COMMON (sa_);\t/* Common data: address family and length.  */\n    char sa_data[14];\t\t/* Address data.  */\n};\n\n// 上面的結構把巨集展開後，等價於下方的資料結構\nstruct sockaddr {\n    unsigned short int sa_family; // 2 bytes\n    char sa_data[14];             // 14 bytes\n};\n```\n\n後來的更新中，為了讓龐大的程式碼可讀性上升，新增了 `sockaddr_in` 的結構用來存取網路相關的應用， `in` 指的是 `internet`，`sockaddr_in` 專門用來存 `IPv4` 的相關地址。\n\n`IPv6` 則是使用 `sockaddr_in6` 結構，在本文章主要會著重在 `IPv4` 相關的範例。\n\n```c\ntypedef uint32_t in_addr_t; // 4 byte\nstruct in_addr {\n    in_addr_t s_addr;\n};\n\nstruct sockaddr_in {    \n    __SOCKADDR_COMMON (sin_);\n    in_port_t sin_port;\t\t\t    /* Port number.  */\n    struct in_addr sin_addr;\t\t/* Internet address.  */\n\n    /* Pad to size of `struct sockaddr'.  */\n    unsigned char sin_zero[sizeof (struct sockaddr)\n\t\t\t   - __SOCKADDR_COMMON_SIZE\n\t\t\t   - sizeof (in_port_t)\n\t\t\t   - sizeof (struct in_addr)];\n};\n\nstruct sockaddr_in {\n    // sa_family_t sin_family\n    unsigned short int sin_family; // 2 bytes\n    unsigned short int sin_port;   // 2 bytes\n    struct in_addr sin_addr;       // 4 bytes\n    unsigned char sin_zero[8];     // 填充，讓 sockaddr_in 的 size 跟 sockaddr 相同\n};\n```\n\n這邊觀看原始碼會覺得奇怪，為什麼還需要使用 `sin_zero` 來做填充的動作。\n\n原因是很多 `socket` 的 `api`，參數都需要填入 `sockaddr`，`sockaddr_in` 則是後來加入的 `struct`。 今天如果我們 `address` 的資料是用 `sockaddr_in` 來儲存，並且想調用相關的函式時，我們就需要強制轉型。 \n\n假設今天用 `socket` 的場景不是網路，也會有對應的結構來存地址，在呼叫 `socket` 通用的 `api` 時，就可以使用強制轉型的方式，讓不同的結構呼叫同一個函式。\n\n實際範例: [unix](https://man7.org/linux/man-pages/man7/unix.7.html)\n\n在後面的例子中也會實際調用，下方的程式碼可以先作為參考。\n\n```c\n#define serverIP\n#define serverPort 12000\n\n// 建立一個 sockaddr_in 結構，存著 server 的相關資料\nstruct sockaddr_in serverAddr = {\n    .sin_family = PF_INET,\n    .sin_addr.s_addr = inet_addr(serverIP),\n    .sin_port = htons(serverPort)\n};\n\nbind(socket_fd, (const struct sockaddr *)\u0026serverAddr, sizeof(serverAddr));\n```\n\n## 位置轉換相關的函數\n\n一般我們在表示 `ip` 位置時都會寫成人類比較容易讀的形式，像是`125.102.25.62`\n\n以 `ipv4` 來說，`address` 是由4個 `byte`，32個 `bit`所組成，在實務上我們常常需要做字串與實際數值(`uint32_t`)的轉換，`linux` 函式庫提供了一系列輔助位置轉換的 `function`。\n\n一般來說，`address` 的實際數值都會用 `in_addr` 或者 `in_addr_t` 來表示\n其本質就是 `uint32_t`，用總共 32 個 `bits` 來表示一個 `IPv4` 的地址\n```c\ntypedef uint32_t in_addr_t; // 4 byte\nstruct in_addr {\n    in_addr_t s_addr;\n};\n```\n\n常用的有以下這五種\n\n- 只能用在 `IPv4` 的處理\n    - inet_addr\n    - inet_aton\n    - inet_ntoa\n- 兼容 `Ipv4` 與 `IPv6`\n    - inet_pton\n    - inet_ntop\n\n使用前必須先\n```c\n#include \u003carpa/inet.h\u003e\n```\n\n### inet_addr\n\n\n```c\nin_addr_t inet_addr(const char *cp)\n```\n\n**功能**: 將字串轉換成數值表示的 `ip address`\n\n**回傳**: 假如輸入的地址合法，會回傳 `uint32_t` 的數值，若不合法則回傳 `INADDR_NONE`\n\n\u003e INADDR_NODE = 0xFFFFFFFF (32 個 bits 全部填一)\n\n[範例程式: inet_addr_ex.c](https://github.com/davidleitw/socket/blob/master/address/inet_addr_ex.c)\n\n### inet_aton\n\n```c\nint inet_aton(const char *string, struct in_addr *addr)\n```\n\n**功能**: 將字串轉換成數值表示的 `ip address`\n\n**回傳**: 轉換成功，會回傳一個非零的值，失敗則會回傳 `0`\n\n[範例程式: inet_aton_ex.c](https://github.com/davidleitw/socket/blob/master/address/inet_aton_ex.c)\n\n### inet_ntoa\n\n```c\nchar *inet_ntoa(struct in_addr)\n```\n\n**功能**: 將 `in_addr` 轉換成字串形式的 `ip address`\n\n**回傳**: 如果沒有錯誤，會傳回成功轉換的字串，失敗時則會回傳 `NULL`\n\n[範例程式: inet_ntoa_ex.c](https://github.com/davidleitw/socket/blob/master/address/inet_ntoa_ex.c)\n\n\u003e [可怕的坑](https://blog.hubert.tw/2009/04/18/%E5%BE%9E-inet_ntoa-%E7%9C%8B-thread-safe-%E7%9A%84-api/)\n\n### inet_pton \u0026 inet_ntop\n\n```c\nconst char *inet_pton(int domain, const void *restrict addr, char *restrict str, socklen_t size)\nint inet_pton(int domain, const char *restrict str, void *restrict addr)\n```\n\n最後這兩個函式是為了因應 `IPv6` 而新增的，除了轉換 `IPv6` 之外，也可以兼容之前 `IPv4` 相關的轉換，本文章主要是介紹 `IPv4` 相關的用法，`IPv6` 的轉換有興趣的可以自己去查資料。\n\n要做 `IPv6` 相關的轉換，要把 `domain` 填入 `AF_INET6` 即可，後面需要搭配 `IPv6` 相關的 `struct`\n\n```c\n#include \u003cstdio.h\u003e\n#include \u003carpa/inet.h\u003e\n\nint main()\n{\n    struct in_addr addr;\n    if (inet_pton(AF_INET, \"8.8.8.8\", \u0026addr.s_addr) == 1) {\n        printf(\"Ip address: %u\\n\", addr.s_addr);\n    }\n\n    char ip_addr[20];\n    if (inet_ntop(AF_INET, \u0026addr.s_addr, ip_addr, sizeof(ip_addr))) {\n        printf(\"After inet_ntop function, ip address: %s\\n\", ip_addr);\n    }\n}\n```\n\n[inet_pton man page](https://man7.org/linux/man-pages/man3/inet_pton.3.html)\n[inet_ntop man page](https://man7.org/linux/man-pages/man3/inet_ntop.3.html)\n\n[範例程式碼 inet_ntop_pton_ex.c](https://github.com/davidleitw/socket/blob/master/address/inet_ntop_pton_ex.c)\n\n轉換相關的 `function` 我每個都寫了一個簡單的範例，可以參考 [完整程式碼](https://github.com/davidleitw/socket/tree/master/address)\n\n## bind\n\n上面介紹了創建一個 `socket` 的方式，也簡單的介紹了存放 `address` 的資料結構，一些常用的轉換函式。\n\n接著我們要介紹 `bind`，這個函式可以讓前面創建的 `socket` 實際綁定到本機的某個 `port` 上面，這樣子 `client` 端在送資料到某個 `port` 的時候，我們寫的 `server` 程式才可以在那個 `port` 上面運行，處理資料。\n\n\u003e attaches a local address to a socket.\n\n```c\nint bind(int sockfd, struct sockaddr *addr, unsigned int addrlen)\n```\n\n#### *sockfd*\n\n一開始呼叫 `socket()` 的回傳值\n\n#### *addr*\n\n`sockaddr` 來描述 `bind` 要綁定的 `address` 還有 `port`。\n\n在先前的介紹有簡單提到，實際存放 `ip address` 的是 `sockaddr_in.sin_addr.s_addr`，如果今天不想綁定 `ip address`，而是單單想綁定某個 `port` 的時候，`s_addr` 就要設成 `INADDR_ANY`，通常會出現在你的主機有多個 `ip` 或者 `ip` 不是固定的情況。\n\n[INADDR_ANY 參考](https://blog.csdn.net/qq_26399665/article/details/52932755)\n\n#### *addrlen*\n\n`addr` 結構的 `size`\n\n#### *return*\n\n如果綁定成功就會回傳 `0`，失敗回傳 `-1`\n\n### example\n```c\n// 建立 socket, 並且取得 socket_fd\nint socket_fd = socket(PF_INET , SOCK_DGRAM , 0);\nif (socket_fd \u003c 0) {\n    printf(\"Fail to create a socket.\");\n}\n    \n// 地址資訊\nstruct sockaddr_in serverAddr = {\n    .sin_family =AF_INET,             // Ipv4\n    .sin_addr.s_addr = INADDR_ANY,    // 沒有指定 ip address\n    .sin_port = htons(12000)          // 綁定 port 12000\n};\n\n// 綁定\n// 因為 bind 可以用在不同種類的 socket，所以是用 sockaddr 宣告\n// 我們用於網路的 address，是用 sockaddr_in 這個結構\n// 在填入的時候要進行強制轉型\n// 前面介紹 sockaddr_in 裡面 sin_zero 就是為了讓兩個結構有相同的 size\nif (bind(socket_fd, (const struct sockaddr *)\u0026serverAddr, sizeof(serverAddr)) \u003c 0) {\n    perror(\"Bind socket failed!\");\n    close(socket_fd);\n    exit(0);\n}\n\nprintf(\"Server ready!\\n\");\n```\n\n## UDP\n\n![](https://i.imgur.com/sxPuuic.png)\n\n接下來就要開始編寫我們的第一支 `socket` 程式，`client` 端輸入小寫的英文字串，`server` 端接收到字串後，將其改成大寫並且送回給 `client` 端。 我們一開始將會透過 `UDP` 協定來實現這個任務。\n\n`UDP` 是一種輕量化的協定，只會提供最低限度的服務，跟 `TCP` 相比，`UDP` 是**非連線導向**的協定，兩個 `process` 之間的溝通並不會事先握手，就像下圖所示，`UDP` 的 `client` 端只會接到指令之後送出，並不會在意對方是否有接收到資料，所以又被稱為 **不可靠的資料傳輸**。\n\n![](https://i.imgur.com/B3WjLDE.png)\n\n![](https://i.imgur.com/Sh21hzp.png)\n\n\n在 `socket` 的 `api` 中，負責 `UDP` 傳送以及接收的 `function` 是 `sendto()`, `recvfrom()`。 因為 `UDP` 協定不需要事先連線，所以只需要有目標 `ip address` 跟 `port` 即可。\n\n### sendto\n\n- [sendto(2) - Linux man page](https://linux.die.net/man/2/sendto)\n\n```c\nssize_t sendto(int sockfd, const void *buf, size_t len, int flags,\n               const struct sockaddr *dest_addr, socklen_t addrlen);\n```\n\n#### *sockfd*\n\n`socket` 的文件描述符\n\n#### *buf*\n\n資料本體\n\n#### *len*\n\n資料長度\n\n#### *flags*\n\n一般填入 `0`，想知道詳細參數意義可以參考 [man page](https://linux.die.net/man/2/sendto)\n\n#### *dest_addr*\n\n目標位置相關資訊\n\n#### *addrlen*\n\n`dest_addr` 的 `size`\n\n#### *return value*\n\n傳送成功時回傳具體傳送成功的 `byte` 數，傳送失敗時會回傳 `-1`\n並且把錯誤訊息存進 [errno](https://man7.org/linux/man-pages/man3/errno.3.html)\n\n### recvfrom\n\n- [recvfrom(2) - Linux man page](https://linux.die.net/man/2/recvfrom)\n\n```c\nssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,\n                 struct sockaddr *src_addr, socklen_t *addrlen);\n```\n\n#### *sockfd*\n\n`socket` 的文件描述符\n\n#### *buf*\n\n接收資料的 `buffer`\n\n#### *len*\n\n資料長度\n\n#### *flags*\n\n一般填入 `0`，想知道詳細參數意義可以參考 [man page](https://linux.die.net/man/2/recvfrom)\n\n#### *src_addr*\n\n資料來源地址，收到訊息之後我們可以一併收到來源地址，透過 `src_addr`，我們才能順利的把處理完的資料發回。\n\n#### *addrlen*\n\n`src_addr` 的 `size`\n\n#### *return value*\n\n接收成功時回傳具體接收成功的 `byte` 數，傳送失敗時會回傳 `-1`\n並且把錯誤訊息存進 [errno](https://man7.org/linux/man-pages/man3/errno.3.html)\n\n### demo\n\n![](https://i.imgur.com/sxPuuic.png)\n\n#### sever example\n\n```c\n#define serverPort 48763\n\n// message buffer\nchar buf[1024] = {0};\n\n// 建立 socket\nint socket_fd = socket(PF_INET , SOCK_DGRAM , 0);\nif (socket_fd \u003c 0){\n    printf(\"Fail to create a socket.\");\n}\n\n// server 地址\nstruct sockaddr_in serverAddr = {\n    .sin_family = AF_INET,           \n    .sin_addr.s_addr = INADDR_ANY,\n    .sin_port = htons(serverPort)\n};\n\n// 將建立的 socket 綁定到 serverAddr 指定的 port\nif (bind(socket_fd, (const struct sockaddr *)\u0026serverAddr, sizeof(serverAddr)) \u003c 0) {\n    perror(\"Bind socket failed!\");\n    close(socket_fd);\n    exit(0);\n}\n\nstruct sockaddr_in clientAddr;\nint len = sizeof(clientAddr);\nwhile (1) {\n    // 當有人使用 UDP 協定送資料到 48763 port\n    // 會觸發 recvfrom()，並且把來源資料寫入 clientAddr 當中\n    if (recvfrom(socket_fd, buf, sizeof(buf), 0, (struct sockaddr *)\u0026clientAddr, \u0026len) \u003c 0) {\n        break;\n    }\n    \n    // 收到 exit 指令就關閉 server\n    if (strcmp(buf, \"exit\") == 0) {\n        printf(\"get exit order, closing the server...\\n\");\n        break;\n    }\n    \n    // 將收到的英文字母換成大寫\n    char *conv = convert(buf);\n    // 顯示資料來源，原本資料，以及修改後的資料\n    printf(\"get message from [%s:%d]: \", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));\n    printf(\"%s -\u003e %s\\n\", buf, conv);\n    \n    // 根據 clientAddr 的資訊，回傳至 client 端\n    sendto(socket_fd, conv, sizeof(conv), 0, (struct sockaddr *)\u0026clientAddr, sizeof(clientAddr));\n    \n    // 清空 message buffer\n    memset(buf, 0, sizeof(buf));\n    free(conv);\n}\n\n// 關閉 socket，並檢查是否關閉成功\nif (close(socket_fd) \u003c 0) {\n        perror(\"close socket failed!\");\n}\n```\n\n#### client example\n\n```c\n#define serverPort 48763\n\n// message buffer\nchar buf[1024] = {0};\nchar recvbuf[1024] = {0};\n\n// 建立 socket\nint socket_fd = socket(PF_INET, SOCK_DGRAM, 0);\nif (socket_fd \u003c 0) {\n    printf(\"Create socket fail!\\n\");\n    return -1;\n}\n\n// server 地址\nstruct sockaddr_in serverAddr = {\n    .sin_family = AF_INET,\n    .sin_addr.s_addr = inet_addr(serverIP),\n    .sin_port = htons(serverPort)\n};\nint len = sizeof(serverAddr);\n\nwhile (1) {\n    // 輸入資料到 buffer\n    printf(\"Please input your message: \");\n    scanf(\"%s\", buf);\n\n    // 傳送到 server 端\n    sendto(socket_fd, buf, sizeof(buf), 0, (struct sockaddr *)\u0026serverAddr, sizeof(serverAddr));\n    // 接收到 exit 指令就退出迴圈\n    if (strcmp(buf, \"exit\") == 0) \n        break;\n\n    // 清空 message buffer\n    memset(buf, 0, sizeof(buf));\n    \n    // 等待 server 回傳轉成大寫的資料\n    if (recvfrom(socket_fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)\u0026serverAddr, \u0026len) \u003c 0) {\n        printf(\"recvfrom data from %s:%d, failed!\", inet_ntoa(serverAddr.sin_addr), ntohs(serverAddr.sin_port));\n    }\n\n    // 顯示 server 地址，以及收到的資料\n    printf(\"get receive message from [%s:%d]: %s\\n\", inet_ntoa(serverAddr.sin_addr), ntohs(serverAddr.sin_port), recvbuf);\n    // 清空 recv buffer\n    memset(recvbuf, 0, sizeof(recvbuf));\n}\n\n// 關閉 socket，並檢查是否關閉成功\nif (close(socket_fd) \u003c 0) {\n        perror(\"close socket failed!\");\n}\n```\n\n想了解細節，可參考 [完整程式碼](https://github.com/davidleitw/socket/tree/master/udp_example)\n\n在 `/udp_example` 下執行 `make` 即可。\n\n![](https://i.imgur.com/ui9e61W.png)\n\n![](https://i.imgur.com/hwQR7X9.png)\n\n---\n\n不知道各位有沒有注意到，我們正式使用 `socket` 的 `api` 時，關於位置的部份都是使用 `sockaddr` 當傳入的參數，我們在網路領域用的 `sockaddr_in` 在傳入時都要再強制轉型一次。\n\n因為 `socket` 本身除了網路通訊之外有很多別的地方也會使用到，為了統一 `api` 操作，所以函式一律是用 `sockaddr` 作為參數，這樣一來各種不同的 `sockaddr_xx` 系列就可以用同一組 `api`，只需要額外轉型即可。\n\n```c\nrecvfrom(socket_fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)\u0026serverAddr, \u0026len)\n```\n\n---\n\n## TCP\n\n接著我們要談談如何用 `socket` 利用 `TCP` 協定來交換資料，首先要知道的是 `TCP` 屬於 **連線導向`Connection-oriented`** 的協定，跟 `UDP` 不同，在雙方交換資料之前必須經過先建立 `TCP connection`，下方是 `socket` 利用 `TCP` 協定溝通的流程圖，可以跟之前提到 `UDP` 的流程圖做一個簡單的對比。\n\n![](https://i.imgur.com/FDOIMj9.png)\n\n\n先從 `server` 端來解說，跟 `UDP` 相比，可以看到 `bind` 完之後多了 `listen` 跟 `accept` 兩個動作。\n\n當 `server` 端創立的 `socket` 成功 `bind` 某個 `port` 之後，他會開始 `listen` 有沒有人申請連線，在 `listen` 這個 `function` 還可以設定 `backlog`，這個參數可以決定今天我們的 `socket` 最多能同時處理的連線要求，避免同時太多人提出連線需求。\n\n\u003e *backlog*: 在 `server` 端 `accept` 之前最多的排隊數量\n\n\n\n`TCP` 協定在建立連線時會經過 **three-way handshake** 流程，下圖是每個流程與 `socket api` 的對應圖。\n\n![](https://i.imgur.com/IK8laxq.png)\n\n\n當 `client` 呼叫 `connect` 時才會開始發起 **three-way handshake**，當 `connect` 結束時，`client` 與 `server` 基本已經完成了整個流程。\n\n那 `server` 端的 `accept` 具體只是從 `server socket` 維護的 `completed connection queue` 中取出一個已完成交握過程的 `socket`。\n\n在 `kernel` 中每個 `socket` 都會維護兩個不同的 `queue`:\n\n- 未完成連線佇列 (***incomplete connection queue***): FIFO with syn_rcvd state\n- 已完成連線佇列 (***complete connection queue***): FIFO with established state\n\n\u003e 所以 accept 根本不參與具體的 ***three-way handshake*** 流程\n\n參考資料 \n\n[socket listen() 分析](https://www.cnblogs.com/codestack/p/11099565.html)\n\n[從 Linux 原始碼看 socket accept](https://www.readfog.com/a/1638167776017354752)\n\n\n**總結一下**\n\n- `server` 端\n    - `listen`: 初始化佇列，準備接受 `connect`\n    - `accept`: 從 `complete connection queue` 中取出一個已連線的 `socket`\n- `client` 端\n    - `connect`: 發起 `three-way handshake`，必須要等 `server` 端開始 `listen` 後才可以使用\n\n## `Client` 端: *connect*\n\n- [connect(2) Linux man page](https://man7.org/linux/man-pages/man2/connect.2.html)\n\n```c\nint connect(int sockfd, const struct sockaddr *addr,\n            socklen_t addrlen);\n```\n\n#### *sockfd* \n\n一開始呼叫 `socket()` 的回傳值\n\n#### *addr*\n\n想要建立連線的 `server` 資料\n\n#### *addrlen*\n\n`addr` 結構的 `size`\n\n#### *return*\n\n錯誤時回傳 `-1`，並且設定 `errno`\n\n## `Server` 端: *listen*\n\n- [listen(2) - Linux man page](https://man7.org/linux/man-pages/man2/listen.2.html)\n\n```c\nint listen(int sockfd, int backlog);\n```\n\n#### *sockfd*\n\n一開始呼叫 `socket()` 的回傳值\n\n#### *backlog*\n\n允許進入 `queue` 的最大連線數量\n\n在 `server` 端還沒有 `accept` 之前，最多能允許幾個 `socket` 申請 `connect`\n\n\u003e 詳細敘述可以參考 [man page](https://man7.org/linux/man-pages/man2/listen.2.html)\n\n#### *return*\n\n錯誤時回傳 `-1`，並且設定 `errno`\n\n## `Server` 端: *accept*\n\n- [accept(2) Linux man page](https://man7.org/linux/man-pages/man2/accept.2.html)\n\n```c\nint accept(int sockfd, struct sockaddr *restrict addr,\n           socklen_t *restrict addrlen);\n```\n\n#### *sockfd*\n\n`server` 端 `socket` 的檔案描述符\n\n#### *addr*\n\n建立 `TCP` 連線的 `Client` 端資料\n\n#### *addrlen* \n\n`addr` 結構的 `size`\n\n#### *return*\n\n返回一個新的 `sock_fd`，專門跟請求連結的 `client` 互動\n\n### demo\n\n![](https://i.imgur.com/T35C7vs.png)\n\n#### server example\n\n```c\n#define serverPort 48763\n\n// message buffer\nchar buf[1024] = {0};\n\n// 建立 socket\nint socket_fd = socket(PF_INET , SOCK_STREAM , 0);\nif (socket_fd \u003c 0){\n    printf(\"Fail to create a socket.\");\n}\n\n// server 地址\nstruct sockaddr_in serverAddr = {\n    .sin_family = AF_INET,\n    .sin_addr.s_addr = INADDR_ANY,\n    .sin_port = htons(serverPort)\n};\n\n// 將建立的 socket 綁定到 serverAddr 指定的 port\nif (bind(socket_fd, (const struct sockaddr *)\u0026serverAddr, sizeof(serverAddr)) \u003c 0) {\n    perror(\"Bind socket failed!\");\n    close(socket_fd);\n    exit(0);\n\n// 初始化，準備接受 connect\n// backlog = 5，在 server accept 動作之前，最多允許五筆連線申請\n// 回傳 -1 代表 listen 發生錯誤\nif (listen(socket_fd, 5) == -1) {\n    printf(\"socket %d listen failed!\\n\", socket_fd);\n    close(socket_fd);\n    exit(0);\n}\n\nprintf(\"server [%s:%d] --- ready\\n\", \n        inet_ntoa(serverAddr.sin_addr), ntohs(serverAddr.sin_port));\n\nwhile(1) {\n    int reply_sockfd;\n    struct sockaddr_in clientAddr;\n    int client_len = sizeof(clientAddr);\n\n    // 從 complete connection queue 中取出已連線的 socket\n    // 之後用 reply_sockfd 與 client 溝通\n    reply_sockfd = accept(socket_fd, (struct sockaddr *)\u0026clientAddr, \u0026client_len);\n    printf(\"Accept connect request from [%s:%d]\\n\", \n            inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));\n    \n    // 不斷接收 client 資料\n    while (recv(reply_sockfd, buf, sizeof(buf), 0)) {\n        // 收到 exit 指令就離開迴圈\n        if (strcmp(buf, \"exit\") == 0) {\n            memset(buf, 0, sizeof(buf));\n            goto exit;\n        }\n\n        // 將收到的英文字母換成大寫\n        char *conv = convert(buf);\n\n        // 顯示資料來源，原本資料，以及修改後的資料\n        printf(\"get message from [%s:%d]: \",\n                inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));\n        printf(\"%s -\u003e %s\\n\", buf, conv);\n\n        // 傳回 client 端\n        // 不需要填入 client 端的位置資訊，因為已經建立 TCP 連線\n        if (send(reply_sockfd, conv, sizeof(conv), 0) \u003c 0) {\n            printf(\"send data to %s:%d, failed!\\n\", \n                    inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));\n            memset(buf, 0, sizeof(buf));\n            free(conv);\n            goto exit;\n        }\n\n        // 清空 message buffer\n        memset(buf, 0, sizeof(buf));\n        free(conv);\n    }\n\n    // 關閉 reply socket，並檢查是否關閉成功\n    if (close(reply_sockfd) \u003c 0) {\n        perror(\"close socket failed!\");\n    }\n}\n```\n\n#### client example\n\n```c\n#define serverPort 48763\n\n // message buffer\nchar buf[1024] = {0};\nchar recvbuf[1024] = {0};\n\n// 建立 socket\nint socket_fd = socket(PF_INET, SOCK_STREAM, 0);\nif (socket_fd \u003c 0) {\n    printf(\"Create socket fail!\\n\");\n    return -1;\n}\n\n// server 地址\nstruct sockaddr_in serverAddr = {\n    .sin_family = AF_INET,\n    .sin_addr.s_addr = inet_addr(serverIP),\n    .sin_port = htons(serverPort)\n};\nint len = sizeof(serverAddr);\n\n// 試圖連結 server，發起 tcp 連線\n// 回傳 -1 代表 server 可能還沒有開始 listen\nif (connect(socket_fd, (struct sockaddr *)\u0026serverAddr, len) == -1) {\n    printf(\"Connect server failed!\\n\");\n    close(socket_fd);\n    exit(0);\n}\n\nprintf(\"Connect server [%s:%d] success\\n\",\n            inet_ntoa(serverAddr.sin_addr), ntohs(serverAddr.sin_port));\n\nwhile (1) {\n    // 輸入資料到 buffer\n    printf(\"Please input your message: \");\n    scanf(\"%s\", buf);\n\n    // 傳送到 server 端\n    if (send(socket_fd, buf, sizeof(buf), 0) \u003c 0) {\n        printf(\"send data to %s:%d, failed!\\n\", \n                inet_ntoa(serverAddr.sin_addr), ntohs(serverAddr.sin_port));\n        memset(buf, 0, sizeof(buf));\n        break;\n    }\n\n    // 接收到 exit 指令就退出迴圈\n    if (strcmp(buf, \"exit\") == 0)\n        break;\n\n    // 清空 message buffer\n    memset(buf, 0, sizeof(buf));\n\n    // 等待 server 回傳轉成大寫的資料\n    if (recv(socket_fd, recvbuf, sizeof(recvbuf), 0) \u003c 0) {\n        printf(\"recv data from %s:%d, failed!\\n\", \n                inet_ntoa(serverAddr.sin_addr), ntohs(serverAddr.sin_port));\n        break;\n    }\n\n    // 顯示 server 地址，以及收到的資料\n    printf(\"get receive message from [%s:%d]: %s\\n\", \n            inet_ntoa(serverAddr.sin_addr), ntohs(serverAddr.sin_port), recvbuf);\n    memset(recvbuf, 0, sizeof(recvbuf));\n}\n\n// 關閉 socket，並檢查是否關閉成功\nif (close(socket_fd) \u003c 0) {\n    perror(\"close socket failed!\");\n}\n```\n![](https://i.imgur.com/S5sMq9b.png)\n\n使用\n\n```bash\nnetstat -a | grep 48763\n```\n\n查看是否建立連線\n\n![](https://i.imgur.com/hnhnqG9.png)\n\n\n\n## localhost\n\n本地端測試網路程式的時候常會使用的地址\n\n可以參考 [wiki](https://zh.wikipedia.org/wiki/Localhost)\n\n\n\n### 參考書籍\n\n![](https://i.imgur.com/Fo0F9ul.png)\n\n#### UNIX Network Programming\n\n![](https://i.imgur.com/XDmFV5u.png)\n\n#### TCP/IP Illustrated\n\n![](https://i.imgur.com/DICEwYi.png)\n\n\n### reference\n- [TCP Socket Programming 學習筆記](http://zake7749.github.io/2015/03/17/SocketProgramming/)\n- [地址轉換函數 inet_addr(), inet_aton(), inet_ntoa()和inet_ntop(), inet_pton()](http://haoyuanliu.github.io/2017/01/15/%E5%9C%B0%E5%9D%80%E8%BD%AC%E6%8D%A2%E5%87%BD%E6%95%B0inet-addr-inet-aton-inet-ntoa-%E5%92%8Cinet-ntop-inet-pton/)\n- [Beej's guide to networking programming](https://beej-zhtw-gitbook.netdpi.net/dao_du)\n- [UDP Server-Client implementation in C](https://www.geeksforgeeks.org/udp-server-client-implementation-c/)","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidleitw%2Fsocket","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavidleitw%2Fsocket","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidleitw%2Fsocket/lists"}