Socket 程式設計入門
什麼是 Socket?
Socket(通訊端)是一種使用「標準 UNIX 檔案描述符」與其他程式通訊的方式。
如你所知,Unix 中一切都是檔案。所以當你想與另一個程式通訊時,你會透過檔案描述符來完成。
Socket 描述符
檔案描述符是一個整數,它與開啟的檔案相關聯,可以是任何東西,例如網路連線、管道、終端機,或是磁碟上的真實檔案。
因為 Socket 描述符是檔案描述符,所以它可以像檔案描述符一樣使用。例如,它可以傳遞給 read 和 write 函式,以便讀取和寫入資料到 Socket。
作業系統使用 Socket 描述符來追蹤開啟的 Socket,並識別資料應該傳送到哪裡或從哪個 Socket 接收。
socket() 函式
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// returns: socket descriptor if OK, -1 on error
函式參數
1. 協定族 (Domain)
協定族是一組協定的集合,它們共享相同的 Socket 位址格式。協定族決定了 Socket 位址的格式,以及 Socket 位址中的哪些欄位用於指定主機和埠號。
常見的協定族有:
AF_INETIPv4 協定族AF_INET6IPv6 協定族AF_UNIXUnix 域協定族AF_UNSPEC未指定協定族
注意:
AF_是 Address Family 的縮寫。AF_INET和AF_INET6域用於 Internet 上的通訊。AF_UNIX和AF_LOCAL域用於同一台機器上的通訊,其中AF_LOCAL是AF_UNIX的別名。AF_UNSPEC域用於指定 Socket 可以與其他域一起使用。
2. Socket 類型 (Type)
Type 參數決定了 Socket 的資料傳輸方式
常見的 Socket 類型有:
| 類型 | 描述 |
|---|---|
| SOCK_STREAM | 提供一個有序、可靠、雙向、連線導向的位元組串流 |
| SOCK_SEQPACKET | 提供一個固定長度、有序、可靠、連線導向的訊息 |
| SOCK_DGRAM | 提供一個固定長度、無連線、不可靠的訊息 |
| SOCK_RAW | 提供一個 IP 的資料報介面 |
注意:
SOCK_SEQPACKET類似於SOCK_DGRAM,但提供了額外的功能,例如錯誤檢查和流量控制,以確保資料可靠地按正確的順序傳遞。
3. 協定 (Protocol)
Protocol 參數指定了 Socket 使用的特定協定。它通常是 0,將根據給定的域和 Socket 類型選擇預設協定。
當同一域和 Socket 類型支援多個協定時,我們可以使用協定參數來選擇特定的協定。
注意:
SOCK_STREAM在AF_INET域中的預設協定是 TCP (RFC 793)。例如:telnet, ftp, ssh, web browsers 等。SOCK_DGRAM在AF_INET域中的預設協定是 UDP (RFC 768)。例如:DNS, DHCP, NTP 等。| 協定 | 描述 | | --- | --- | | IPPROTOIP | IPv4 | | IPPROTOIPV6 | IPv6 | | IPPROTOICMP | Internet 控制訊息協定 | | IPPROTORAW | 原始 IP 資料包 | | IPPROTOTCP | TCP - 傳輸控制協定 | | IPPROTOUDP | UDP - 使用者資料報協定 |
socket 函式如何運作?
呼叫 socket 函式類似於呼叫 open 函式。兩者都回傳一個檔案描述符,可以用於後續的 I/O 操作。
當你完成了 Socket 的使用,可以透過呼叫 close 函式來關閉 Socket,並釋放檔案描述符以便重用。
儘管 Socket 實際上是一個檔案描述符,但是你不能使用它來呼叫任何接受檔案描述符作為參數的函式。例如:lseek,因為 Socket 沒有檔案偏移量。
下面列舉了一些常見的檔案描述符操作,以及 Socket 是否支援這些操作。
| 函式 | 描述 |
|---|---|
| dup | 複製 Socket |
| close | 釋放 Socket |
| read | 從 Socket 讀取資料,相當於 recv |
| write | 向 Socket 寫入資料,相當於 send |
| select | 等待 Socket 準備好進行 I/O 操作 |
| mmap | 不支援 |
| lseek | 不支援 |
Socket 位址
為了確定我們要與之通訊的特定程序,我們需要知道程序的位址。
程序位址由 機器的網路位址 和 埠號 構成。
位元組序

image
當我們與同一台機器上的程序通訊時,我們不需要擔心資料的位元組順序。
但是,當我們與不同機器上的程序通訊時,我們需要確保資料的位元組順序在兩台機器上是相同的。
大端序和小端序
大端序:資料的低位元組存放在記憶體的高位址,高位元組存放在低位址。
大端序的排列方式與資料用位元組表示時的書寫順序一致,符合人類的閱讀習慣。
big-endian example:
| n | n + 1 | n + 2 | n +3 |
|-----|-------|-------|------|
| MSB | | | LSB |小端序: 將一個多位數的低位放在較小的位址處,高位放在較大的位址處,則稱小端序。
小端序與人類的閱讀習慣相反,但更符合電腦讀取記憶體的方式,因為CPU讀取記憶體中的資料時,是從低位址向高位址方向進行讀取的。
| n + 3 | n + 2 | n + 1 | n |
|-------|-------|-------|-----|
| MSB | | | LSB |通常,網路協定將指定位元組順序,以便資料可以在不混淆位元組順序的情況下在不同的機器之間交換。
- TCP/IP 協定套件使用大端序。
位元組序轉換函式
| 函式 | 描述 |
|---|---|
| htons | 主機位元組序到網路位元組序的轉換(16位元) |
| htonl | 主機位元組序到網路位元組序的轉換(32位元) |
| ntohs | 網路位元組序到主機位元組序的轉換(16位元) |
| ntohl | 網路位元組序到主機位元組序的轉換(32位元) |
h 是 host 的縮寫,n 是 network 的縮寫。
l 是 long 的縮寫,s 是 short 的縮寫。
下面是這些函式的原型:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
位址結構
一個位址標識了一個特定協定通訊域中的一個 Socket 端點。
在不同的作業系統可能會有不同的位址結構實作。
例如在 FreeBSD 中,位址結構定義如下:
struct sockaddr {
unsigned char sa_len; /* 總長度 */
sa_family_t sa_family; /* 位址族,如 AF_INET */
char sa_data[14]; /* 固定長度的位址資訊 */
};
在 Linux 系統中,位址結構如下:
struct sockaddr {
sa_family_t sa_family; /* 協定族,如 AF_INET */
char sa_data[14]; /* 固定長度的位址資訊 */
};
sa_family 通常會是 AFINET(IPv4)或 AFINET6(IPv6)。
sa_data 是一個長度為 14 位元組的字元陣列,用於存放位址資訊,例如目標 IP 位址和埠號。
IPv4 位址結構
IPv4 位址結構在標頭檔netinet/in.h中定義如下:
// 定義在 <sys/socket.h>
typedef uint16_t sa_family_t; // short, 2 位元組
// 定義在 <netinet/in.h>
typedef uint16_t in_port_t; // unsigned short, 2 位元組
typedef uint32_t in_addr_t; // unsigned long, 4 位元組
struct in_addr {
in_addr_t s_addr; /* 32 位元 IPv4 位址, 網路位元組序(大端序) */
};
struct sockaddr_in {
sa_family_t sin_family; /* 位址族,如 AF_INET */
in_port_t sin_port; /* 埠號,網路位元組序 */
struct in_addr sin_addr; /* IPv4 位址,網路位元組序 */
unsigned char sin_zero[8]; /* 用於填充,使其總長度為 16 位元組 */
};
IPv6 位址結構
IPv6 位址結構在標頭檔netinet/in.h中定義如下:
// 定義在 <sys/socket.h>
typedef uint16_t sa_family_t; // short, 2 位元組
// 定義在 <netinet/in.h>
typedef uint16_t in_port_t; // unsigned short, 2 位元組
struct in6_addr {
unsigned char s6_addr[16]; // IPv6 位址,16 位元組
};
// Defined in <netinet/in.h>
struct sockaddr_in6 {
sa_family_t sin6_family; /* 位址族,如 AF_INET6, 2 位元組 */
in_port_t sin6_port; /* 埠號,網路位元組序,2 位元組 */
uint32_t sin6_flowinfo; /* IPv6 流資訊,4 位元組 */
struct in6_addr sin6_addr; /* IPv6 位址,16 位元組 */
uint32_t sin6_scope_id; /* IPv6 作用域 ID,4 位元組 */
};
// 共計 28 位元組
儘管 sockaddr_in 和 sockaddr_in6 結構體不同,但是它們都會被轉換為 sockaddr 結構體傳遞給 socket 系統呼叫。
sockaddr_storage 結構
sockaddr_storage 結構體被設計為足夠大,可以容納任何位址結構。因為你不知道你將要處理的位址結構是什麼,所以你可以使用這個結構體來容納任何位址結構,然後在需要使用它時將其轉換為適當的類型。
sockaddr_storage 結構體在標頭檔sys/socket.h中定義如下:
struct sockaddr_storage {
sa_family_t ss_family; /* 2 位元組, 位址族,如 AF_INET, AF_INET6 */
// 以下欄位是實作定義的
char __ss_pad1[_SS_PAD1SIZE]; /* 6 位元組 (size_t)_SS_PAD1SIZE = 6 */
int64_t __ss_align; /* 8 位元組 */
char __ss_pad2[_SS_PAD2SIZE]; /* 112 位元組 (size_t)_SS_PAD2SIZE = 112 */
};
// 共計 128 位元組
IP 位址格式轉換
常見的 IP 位址格式如 192.168.0.1 是一種數字和點的格式,但是在電腦中儲存的是二進位格式。
為了印出可讀的位址格式,我們使用 inet_ntop 函式將電腦中二進位的位址轉換為數字和點的格式,其中 ntop 代表 "network to presentation"。
為了將可讀的位址格式轉換為二進位格式,我們使用 inet_pton 函式,其中 pton 代表 "presentation to network"。
這兩個函式都是執行緒安全的。
函式原型
inet_ntop 和 inet_pton 函式在標頭檔 arpa/inet.h 中定義如下:
#include <arpa/inet.h>
const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socklen_t size);
// 回傳值:若成功則回傳指向 str 的指標,若出錯則回傳 NULL
int inet_pton(int domain, const char *restrict str, void *restrict addr);
// 回傳值:若成功則回傳 1,若輸入不是有效的位址字串則回傳 0,若出錯則回傳 -1
回傳值
- inet_ntop:若成功則回傳指向 str 的指標,若出錯則回傳 NULL
- inet_pton:若成功則回傳 1,若輸入不是有效的位址字串則回傳 0,若出錯則回傳 -1
參數說明
- domain: 位址族,如 AFINET, AFINET6
- addr: 指向二進位位址的指標
- str: 指向儲存可讀位址的緩衝區的指標
- size: str 緩衝區的大小
通常,我們會使用
INET_ADDRSTRLEN或INET6_ADDRSTRLEN巨集來指定緩衝區的大小,它們的值如下:- INET_ADDRSTRLEN :用於 IPv4 位址,它的值為 16 位元組
- INET6_ADDRSTRLEN :用於 IPv6 位址,它的值為 46 位元組
範例程式碼
struct sockaddr_in sa;
struct sockaddr_in6 sa6;
inet_pton(AF_INET, "10.12.110.57", &(sa.sin_addr));
inet_pton(AF_INET6, "2001:db8:63b3:1::3490", &(sa6.sin6_addr));
// IPv4:
char ip4[INET_ADDRSTRLEN]; // 用於儲存 IPv4 位址的字串
struct sockaddr_in sa; // 假設這個結構體已經被載入了一些東西
inet_ntop(AF_INET, &(sa.sin_addr), ip4, INET_ADDRSTRLEN);
printf("The IPv4 address is: %s\n", ip4);
// IPv6:
char ip6[INET6_ADDRSTRLEN]; // 用於儲存 IPv6 位址的字串
struct sockaddr_in6 sa6; // 假設這個結構體已經被載入了一些東西
inet_ntop(AF_INET6, &(sa6.sin6_addr), ip6, INET6_ADDRSTRLEN);
printf("The address is: %s\n", ip6);
注意:
上面的程式碼不夠穩健,因為沒有檢查 IP 位址是否有效。
inet_pton 函式在出錯時回傳 -1,如果輸入不是指定位址族中有效的位址,則回傳 0,成功時回傳 1。
因此,檢查回傳值是否大於 0,以確保轉換成功。
取得主機名稱和 IP 位址
getaddrinfo()
在舊的 Unix 系統中,有兩個函式可以將主機名稱轉換為 IP 位址,或將 IP 位址轉換為主機名稱。它們是 gethostbyname 和 gethostbyaddr。
但是,現在使用 getaddrinfo 和 getnameinfo 函式來代替它們,因為它們更加穩健,更加可移植。
下面是 getaddrinfo 函式的原型:
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *node, // e.g. "www.example.com" or IP
const char *service, // e.g. "http" or port number
const struct addrinfo *hints,
struct addrinfo **res);
void freeaddrinfo(struct addrinfo *res);
getaddrinfo() 函式有三個輸入參數,然後將結果儲存在 res 中,它回傳一個整數,如果成功則為 0,如果出錯則為非 0 值。
第一個參數,node,是主機名稱或 IP 位址。如果它是 NULL,則 IP 位址設定為 IPv4 的 INADDRANY 或 IPv6 的 INADDR6ANY_INIT。
第二個參數,service,是服務名稱或埠號。如果它是 NULL,則埠號設定為 0。它可以是十進位數或服務名稱,例如 "http" 或 "ftp"。更多檢視:IANA 常見埠號列表
第三個參數,hints,是一個指向 struct addrinfo 的指標。它用於指定我們想要的 Socket 類型。如果它是 NULL,則預設為 SOCKSTREAM(TCP)和 AFINET(IPv4)
第四個參數,res,是一個指向 struct addrinfo 的指標的指標。它將指向一個鏈結串列,其中包含主機名稱或 IP 位址的資訊。
下面是 getaddrinfo() 的用法範例:
struct addrinfo{
int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
int ai_family; // AF_INET, AF_INET6, AF_UNSPEC
int ai_socktype; // SOCK_STREAM, SOCK_DGRAM
int ai_protocol; // use 0 for "any"
size_t ai_addrlen; // size of ai_addr in bytes
struct sockaddr *ai_addr; // struct sockaddr_in or _in6
char *ai_canonname; // full canonical hostname
struct addrinfo *ai_next; // linked list, next node
};
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
memset(&hints, 0, sizeof(struct addrinfo)); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // Don't care if IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
hints.ai_flags = AI_PASSIVE; // Fill in my IP for me
if ((status = getaddrinfo(NULL, "3490", &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
exit(1);
}
// servinfo 現在指向一個結構體 addrinfo 的鏈結串列,鏈結串列中有 1 個或多個結構體
// ... do everything until you don't need servinfo anymore ....
freeaddrinfo(servinfo); // 釋放 servinfo 結構體
下面是一個用戶端的範例,它想連接到伺服器(www.example.com)的 3490 埠。
此時並沒有連接到伺服器,但它為之後的連線做了準備。
int status;
struct addrinfo hints;
struct addrinfo *servinfo; // will point to the results
memset(&hints, 0, sizeof hints); // make sure the struct is empty
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
// get ready to connect
status = getaddrinfo("www.example.com", "3490", &hints, &servinfo);
getnameinfo()
getnameinfo() 函式是 getaddrinfo() 函式的逆函式:它將 Socket 位址轉換為相應的主機和服務。
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *serv, size_t servlen, int flags);
函式回傳值
如果成功,回傳 0,然後 node 和 service 名稱將填充為 null-terminated 字串,可能會截斷以適合指定的緩衝區長度。
如果出錯,回傳一個非零錯誤代碼,緩衝區的內容是未定義的。
範例程式碼
struct sockaddr_in6 sa; // could be IPv4 if you want
char host[1024];
char service[20];
// pretend sa is full of good information about the host and port...
getnameinfo(&sa, sizeof sa, host, sizeof host, service, sizeof service, 0);
printf("host: %s\n", host); // e.g. "www.example.com"
printf("service: %s\n", service); // e.g. "http"
bind() 函式
函式用途
當一個 socket 被建立後,它必須綁定到一個位址和埠號,以便其他程序可以連接到它。
通常伺服器會呼叫 bind 函式來綁定一個 socket 到一個特定的埠號和 IP 位址。
函式原型
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
函式參數
sockfd: Socket 檔案描述符my_addr: 指向 struct sockaddr 的指標,它包含伺服器的埠號和 IP 位址。addrlen: my_addr (struct sockaddr) 的長度。
函式回傳值
如果成功,回傳 0,如果出錯,回傳 -1 並設定 errno。
範例程式碼 - 手動設定 IP 位址和埠號
// !!! THIS IS THE OLD WAY !!!
int sockfd;
struct sockaddr_in my_addr;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(MYPORT); // short, network byte order
my_addr.sin_addr.s_addr = inet_addr("10.12.110.57");
memset(my_addr.sin_zero, '\0', sizeof my_addr.sin_zero);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr);
範例程式碼 - 使用 getaddrinfo()
struct addrinfo hints, *res;
int sockfd;
// 首先,使用 getaddrinfo() 函式載入位址結構:
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me (IP of the host it’s running on)
getaddrinfo(NULL, "3490", &hints, &res);
// 然後,使用 socket() 函式建立一個 socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// 最後,使用 bind() 函式將 socket 綁定到指定的埠號和 IP 位址:
bind(sockfd, res->ai_addr, res->ai_addrlen);
connect() 函式
函式用途
通常用戶端會呼叫 connect 函式來連接到伺服器。
函式原型
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
函式參數
sockfd: Socket 檔案描述符serv_addr: 指向 struct sockaddr 的指標,它包含伺服器的埠號和 IP 位址。addrlen: serv_addr (struct sockaddr) 的長度。
函式回傳值
如果成功,回傳 0,如果出錯,回傳 -1 並設定 errno。
範例程式碼
struct addrinfo hints, *res;
int sockfd;
// 首先,使用 getaddrinfo() 函式載入位址結構:
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
// 其次,使用 socket() 函式建立一個 socket:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
// 最後,使用 connect() 函式連接到伺服器:
connect(sockfd, res->ai_addr, res->ai_addrlen);
注意:在上面的範例程式碼中,我們沒有呼叫 bind() 函式。這是因為我們不關心本地埠號。我們只關心連接方的埠號。這種情況,作業系統核心會自動為我們分配一個埠號,而遠端伺服器會自動從我們那裡取得該埠號。
listen() 函式
函式用途
伺服器呼叫 listen 函式來監聽來自用戶端的連線請求。
函式原型
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
函式參數
sockfd: Socket 檔案描述符backlog: 最大的連線請求佇列的長度。如果佇列滿了,用戶端會收到一個錯誤訊息,錯誤碼為 ECONNREFUSED。
函式回傳值
如果成功,回傳 0,如果出錯,回傳 -1 並設定 errno。
範例程式碼
getaddrinfo();
socket();
bind();
listen();
/* accept() goes here */
如果要讓伺服器執行在指定的埠號上,需要在呼叫 listen 函式之前呼叫 bind 函式。
accept() 函式
函式用途
通常伺服器會呼叫 accept 函式來接受來自特定埠號的用戶端的連線請求。
函式原型
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函式參數
sockfd: Socket 檔案描述符addr: 指向 struct sockaddr 的指標,它包含用戶端的埠號和 IP 位址。addrlen: addr (struct sockaddr) 的長度。
函式回傳值
如果成功,回傳一個新的 Socket 檔案描述符(非負整數),如果出錯,回傳 -1 並設定 errno。
範例程式碼
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#define MYPORT "3490" // the port users will be connecting to
#define BACKLOG 10 // how many pending connections queue will hold
int main(void){
struct sockaddr_storage their_addr;
socklen_t addr_size;
struct addrinfo hints, *res;
int sockfd, new_fd;
// first, load up address structs with getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; // use IPv4 or IPv6, whichever
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
getaddrinfo(NULL, MYPORT, &hints, &res);
// make a socket, bind it, and listen on it:
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);
listen(sockfd, BACKLOG);
// now accept an incoming connection:
addr_size = sizeof their_addr;
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &addr_size);
// ready to communicate on socket descriptor new_fd!
}
注意:
所有的 send() 和 recv() 函式都是在 accept() 回傳的新的 Socket 檔案描述符上呼叫的。
如果只想接受一個連線,可以在呼叫
accept函式之後呼叫close(sockfd)來關閉監聽 Socket,以防止其他連線。
send() 和 recv() 函式
函式用途
send 和 recv 函式用於用戶端和伺服器之間的串流 Socket 或已連線的資料報 Socket (datagram socket) 之間的通訊。
對於未連線的資料報 Socket,使用 sendto 和 recvfrom 函式。
函式原型 - send()
#include <sys/types.h>
#include <sys/socket.h>
int send(int sockfd, const void *msg, int len, int flags);
函式參數 - send()
sockfd: Socket 檔案描述符msg: 指向要傳送的訊息的緩衝區的指標。len: 要傳送的訊息的長度 (以位元組為單位)。flags: 標誌符,用於修改 send() 的行為。
函式回傳值 - send()
回傳實際傳送的位元組數,其值可能小於 len 參數的值。
如果出錯,回傳 -1 並設定 errno。
範例程式碼 - send()
char *msg = "Hello, world!";
int len;
int bytes_sent;
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
函式原型 - recv()
#include <sys/types.h>
#include <sys/socket.h>
int recv(int sockfd, void *buf, size_t len, int flags);
函式參數 - recv()
sockfd: Socket 檔案描述符buf: 指向接收緩衝區的指標。len: 緩衝區的最大長度 (以位元組為單位)。flags: 標誌符,用於修改 recv() 的行為。
函式回傳值 - recv()
回傳實際讀取到緩衝區的位元組數。
如果出錯,回傳 -1 並設定 errno。
如果連線已被關閉,回傳 0。
範例程式碼 - recv()
char buf[100];
int bytes_received;
bytes_received = recv(sockfd