Berkeley套接字

**柏克萊插座**（英語：Internet Berkeley sockets） ，又稱為 **BSD 插座**(BSD sockets) 是一種應用程式介面（API），用於網路插座（ socket）與 Unix 域插座，包括了一個用 C 語言寫成的應用程式開發庫，主要用於實現行程間通訊，在電腦網路通訊方面被廣泛使用。

Berkeley 插座（也作 BSD 插座應用程式介面）剛開始是 4.2BSD Unix 作業系統（於 1983 發布）的一套應用程式介面。然而，由於 AT&T 的專利保護著 UNIX，所以只有在 1989 年柏克萊大學才能自由地發布自己的作業系統和網路庫。

Berkeley 插座應用程式介面形成了事實上的網路插座的標準精髓。 大多數其他的程式語言使用與這套用 C 語言寫成的應用程式介面 類似的介面。 這套應用程式介面也被用於 Unix 域插座（Unix domain sockets），後者可以在單機上為行程間通訊（IPC）的介面。

這種基於流的傳輸層介面（TLI）為插座應用程式介面提供了一種選擇。 不過，最近提供 TLI 應用程式介面的的系統同時也提供 Berkeley 插座應用程式介面。

## Berkeley 插座介面

**Berkeley 插座介面**，一個應用程式介面（API），使用一個 Internet 插座的概念，使主機間或者一台電腦上的行程間可以通訊。 它可以在很多不同的輸入 / 輸出裝置和驅動之上執行，儘管這有賴於作業系統的具體實現。 介面實現用於 TCP/IP 協定，因此它是維持 Internet 的基本技術之一。 它是由加利福尼亞的柏克萊大學開發，最初用於 Unix 系統。 如今，所有的現代作業系統都有一些源於 Berkeley 插座介面的實現，它已成為連接 Internet 的標準介面。

插座介面的接入有三個不同的級別，最基礎的也是最有效的就是 raw socket 級別接入。 很少的應用程式需要在外向通訊控制的這個級別接入，所以 raw socket 級別是只為了用於開發電腦 Internet 相關技術的。 最近幾年，大多數的作業系統已經實現了對它的全方位支援，包括 Windows XP。

## 使用 Berkeley 插座的系統

由於 Berkeley 插座是第一個 socket，大多數程式設計師很熟悉它們，所以大量系統把柏克萊插座作為其主要的網路 API。一個不完整的列表如下：

* Windows Sockets (Winsock) ，和 Berkeley Sockets 很相似，最初是為了便於移植 Unix 程式。
* Java Sockets
* Python sockets
* Perl sockets

## 標頭檔

Berkeley 插座介面的定義在幾個標頭檔中。這些檔案的名字和內容與具體的實現之間有些許的不同。 大體上包括：

　
: **`<sys/socket.h>`**
: 核心 BSD 插座核心函式和資料結構。

　
: AF\_INET、AF\_INET6 位址集和它們相應的協定集 PF\_INET、PF\_INET6. 廣泛用於 Internet，這些包括了 IP 位址和 TCP、UDP 埠號。

　
: **`<netinet/in.h>`**
: AF\_INET 和 AF\_INET6 位址家族和他們對應的協定家族 PF\_INET 和 PF\_INET6。在網際網路編程中廣泛使用，包括 IP 位址以及 TCP 和 UDP 埠號。

　
: **`<sys/un.h>`**
: PF\_UNIX/PF\_LOCAL 位址集。用於執行在一台電腦上的程式間的本地通信，不用於網路通訊。

　
: **`<arpa/inet.h>`**
: 處理數值型 IP 位址的函式。

　
: **`<netdb.h>`**
: 將協定名和主機名翻譯為數值位址的函式。搜尋本地資料以及 DNS。

## 插座 API 函式

這個列表是一個 Berkeley 插座 API 庫提供的函式或者方法的概要：

* `socket()` 建立一個新的確定類型的插座，類型用一個整型數值標識（檔案描述子），並為它分配系統資源。
* `bind()` 一般用於伺服器端，將一個插座與一個插座位址結構相關聯，比如，一個指定的本地埠和 IP 位址。
* `listen()` 用於伺服器端，使一個繫結的 TCP 插座的 tcp 狀態由 CLOSE 轉至 LISTEN；作業系統核心為此監聽 socket 所對應的 tcp 伺服器建立一個 pending socket 佇列和一個 established socket 佇列；參數 backlog 指定 pending socket 佇列的長度，0 表示長度可以無限大。pending socket，就是某客戶端三次握手的 syn 包到達，核心為這個 syn 包對應的 tcp 請求生成一個 socket（狀態為 SYN\_RECV），但三次握手還沒有完成時的 socket。
* `connect()` 用於客戶端，為一個插座分配一個自由的本地埠號。 如果是 TCP 插座的話，它會試圖獲得一個新的 TCP 連接。
* `accept()` 用於伺服器端。 它接受一個從遠端客戶端發出的建立一個新的 TCP 連接的接入請求，建立一個新的插座，與該連接相應的插座位址相關聯。
* `send()` 和 `recv()`, 或者 `write()` 和 `read()`, 或者 `recvfrom()` 和 `sendto()`, 用於往 / 從遠端插座傳送和接受資料。
* `close()` 用於系統釋放分配給一個插座的資源。 如果是 TCP，連接會被中斷。
* `gethostbyname()` 和 `gethostbyaddr()` 用於解析主機名和位址。
* `select()` 用於修整有如下情況的插座列表： 準備讀，準備寫或者是有錯誤。
* `poll()` 用於檢查插座的狀態。 插座可以被測試，看是否可以寫入、讀取或是有錯誤。
* `getsockopt()` 用於查詢指定的插座一個特定的插座選項的當前值。
* `setsockopt()` 用於為指定的插座設定一個特定的插座選項。

更多的細節如下給出。

### socket()

`socket()` 為通訊建立一個端點，為插座返回一個檔案描述子。 socket () 有三個參數：

* domain 為建立的插座指定協定集（或稱做位址族 address family)。 例如：
  * `AF_INET` 表示 IPv4 網路協定
  * `AF_INET6` 表示 IPv6
  * `AF_UNIX` 表示本地插座（使用一個檔案）
* type（socket 類型）如下：
  * `SOCK_STREAM` （可靠的面向流服務或流插座）
  * `SOCK_DGRAM` （資料報服務或者資料報插座）
  * `SOCK_SEQPACKET` （可靠的連續封包服務）
  * `SOCK_RAW` (在網路層之上自行指定運輸層協定頭，即原始插座)
* protocol 指定實際使用的傳輸協定。 最常見的就是 `IPPROTO_TCP`、`IPPROTO_SCTP`、`IPPROTO_UDP`、`IPPROTO_DCCP`。這些協定都在 <netinet/in.h> 中有詳細說明。 如果該項為「`0`」的話，即根據選定的 domain 和 type 選擇使用預設協定。

如果發生錯誤，函式返回值為 - 1。 否則，函式會返回一個代表新分配的描述符的整數。

**原型：**

### bind()

`bind()` 為一個插座分配位址。當使用 `socket()` 建立插座後，只賦予其所使用的協定，並未分配位址。在接受其它主機的連接前，必須先呼叫 `bind()` 為插座分配一個位址。`bind()` 有三個參數：

* `sockfd`, 表示使用 bind 函式的插座描述符
* `my_addr`, 指向 sockaddr 結構（用於表示所分配位址）的指標
* `addrlen`, 用 socklen\_t 欄位指定了 sockaddr 結構的長度

如果發生錯誤，函式返回值為 - 1，否則為 0。

**原型**
:

### listen()

當 socket 和一個位址繫結之後，`listen()` 函式會開始監聽可能的連接請求。然而，這只能在有可靠資料流保證的時候使用，例如：資料類型 (`SOCK_STREAM`, `SOCK_SEQPACKET`)。

listen () 函式需要兩個參數：

* `sockfd`, 一個 socket 的描述符.
* `backlog`, 完成三次握手、等待 accept 的全連接的佇列的最大長度上限。對於 AF\_INET 類型的 socket，全連接數量為：min (backlog, somaxconn)。當佇列滿時，新的全連接會返回錯誤。somaxconn 預設為 128. 半連接佇列的最大長度可通過 sysctl 函式設定 tcp\_max\_syn\_backlog，預設值為 256。Linux Kernel 2.2 之後，全連接佇列與半連接佇列分別叫做 accept queue 與 syns queue。根據 /proc/sys/net/ipv4/tcp\_abort\_on\_overflow 里的值為 0 表示如果三次握手第三步的時候全連接佇列滿了，那麼 server 扔掉 client 發過來的 ack，server 過一段時間再次傳送 syn+ack 給 client（也就是重新走握手的第二步），如果 client 逾時等待比較短，就很容易異常；tcp\_abort\_on\_overflow 為 1 表示第三次握手時如果全連接佇列滿了，server 傳送一個 reset 包給 client，表示廢掉這個握手過程和這個連接。

一旦連接被接受，返回 0 表示成功，錯誤返回 - 1。

**原型**:

### accept()

當應用程式監聽來自其他主機的面對資料流的連接時，通過事件（比如 Unix select () 系統呼叫）通知它。必須用 `accept()` 函式初始化連接。 `accept()` 為每個連接創立新的插座並從監聽佇列中移除這個連接。它使用如下參數：

* `sockfd`, 監聽的插座描述符
* `cliaddr`, 指向 sockaddr 結構體的指標，客戶機位址資訊。
* `addrlen`, 指向 `socklen_t` 的指標，確定客戶機位址結構體的大小 。

返回新的插座描述符，出錯返回 - 1。進一步的通信必須通過這個插座。

Datagram 插座不要求用 accept () 處理，因為接收方可能用監聽插座立即處理這個請求。

**函式原型：**

### connect()

`connect()` 系統呼叫為一個插座設定連接，參數有檔案描述子和主機位址。

某些類型的插座是無連接的，大多數是 UDP 協定。對於這些插座，連接時這樣的：預設傳送和接收資料的主機由給定的位址確定，可以使用 send () 和 recv ()。
返回 - 1 表示出錯，0 表示成功。

**函式原型：**

### select()

* 第一個參數 nfds：沒有用，僅僅為與柏克萊 Socket 相容而提供。
* 第二個參數 readfds：指定一個 Socket 陣列，select 檢查該陣列中的所有 Socket。如果成功返回，則 readfds 中存放的是符合『可讀性』條件的陣列成員（如緩衝區中有可讀的資料）。
* 第三個參數 writefds：指定一個 Socket 陣列，select 檢查該陣列中的所有 Socket。如果成功返回，則 writefds 中存放的是符合『可寫性』條件的陣列成員（包括連接成功）。
* 第四個參數 exceptfds：指定一個 Socket 陣列，select 檢查該陣列中的所有 Socket。如果成功返回，則 cxceptfds 中存放的是符合『有異常』條件的陣列成員（包括連接接失敗）。
* 第五個參數 timeout：指定 select 執行的最長時間，如果在 timeout 限定的時間內，readfds、writefds、exceptfds 中指定的 Socket 沒有一個符合要求，就返回 0。

### getsockname () 和 getpeername ()

getsockname 函式取得已繫結（可能是未呼叫 bind 的系統自動繫結）的套介面本地協定位址。

getpeername 函式獲得與指定套介面連接的遠端資訊（IP:PORT）。

### gethostbyname () 和 gethostbyaddr ()

`gethostbyname()` 和 `gethostbyaddr()` 函式是用來解析主機名和位址的。可能會使用 DNS 服務或者本地主機上的其他解析機制（例如查詢 /etc/hosts）。返回一個指向 struct hostent 的指標，這個結構體描述一個 IP 主機。函式使用如下參數：

* name 指定主機名。例如 www.wikipedia.org
* addr 指向 struct in\_addr 的指標，包含主機的位址。
* len 給出 addr 的長度，以位元組為單位。
* type 指定位址族類型 （比如 AF\_INET）。

出錯返回 NULL 指標，可以通過檢查 h\_errno 來確定是臨時錯誤還是未知主機。正確則返回一個有效的 struct hostent \*。

這些函式並不是柏克萊插座嚴格的組成部分。這些函式可能是過時了，只能處理 IPv4 位址。在 IPv6 中，替代的新函式是 getaddrinfo () and getnameinfo (), 這些新函式是基於 _addrinfo_ 資料結構。參考 <Ws2tcpip.h>。

**函式原型：**

### setsockopt()

setsockopt 函式用來設定插座選項。

參數：

* sockfd: 插座
* level: 協定層 SOL\_SOCKET/IPPROTO\_IP/IPPRO\_TCP
* optname: 選項名 每一個協定層都有其固定的選項名
* optval: 緩衝區 set 是指向將要存放的位址，get 是指向目前存放資訊的位址
* optlen: 緩衝區大小長度

在 socket 層，有以下一些選項:

* SO\_BROADCAST 允許傳送廣播資料 int
* SO\_DEBUG　　　　　　　　允許除錯　　　　　　　　　　　　　　　　int
* SO\_DONTROUTE　　　　　　不尋找路由　　　　　　　　　　　　　　　int
* SO\_ERROR　　　　　　　　獲得插座錯誤　　　　　　　　　　　　　int
* SO\_KEEPALIVE　　　　　　保持連接　　　　　　　　　　　　　　　　int
* SO\_LINGER　　　　　　　 延遲關閉連接　　　　　　　　　　　　　　struct linger
* SO\_OOBINLINE　　　　　　帶外資料放入正常資料流　　　　　　　　　int
* SO\_RCVBUF　　　　　　　 接收緩衝區大小　　　　　　　　　　　　　int
* SO\_SNDBUF　　　　　　　 傳送緩衝區大小　　　　　　　　　　　　　int
* SO\_RCVLOWAT　　　　　　 接收緩衝區下限　　　　　　　　　　　　　int
* SO\_SNDLOWAT　　　　　　 傳送緩衝區下限　　　　　　　　　　　　　int
* SO\_RCVTIMEO　　　　　　 接收逾時　　　　　　　　　　　　　　　　struct timeval
* SO\_SNDTIMEO　　　　　　 傳送逾時　　　　　　　　　　　　　　　　struct timeval
* SO\_REUSERADDR　　　　　 允許重用本地位址和埠　　　　　　　　　int
* SO\_TYPE　　　　　　　　 獲得插座類型　　　　　　　　　　　　　int
* SO\_BSDCOMPAT　　　　　　與 BSD 系統相容　　　　　　　　　　　　　 int

### ioctlsocket

根據第二個參數的取值，設定 socket I/O 模式：

* FIONBIO：允許設定 socket 為阻塞或非阻塞：當第三個參數 argp 為 0 是阻塞模式，為非 0 則為非阻塞模式。如果已對一個套介面進行了 WSAAsynSelect () 操作，則任何用 ioctlsocket () 來把套介面重新設定成阻塞模式的試圖將以 WSAEINVAL 失敗。為了把套介面重新設定成阻塞模式，應用程式必須首先用 WSAAsynSelect () 呼叫（IEvent 參數置為 0）來禁至 WSAAsynSelect (), 或者通過設定 lNetworkEvents 參數為 0 來呼叫 WSAEventSelect。
* FIONREAD：返回插座 s 下一次自動讀入的資料量的大小。用來確定（determin）懸掛（pending）在網路輸入緩衝區中，能從 socket s 中讀取的資料總數。返回單次 recv 函式能讀取的資料的總數
* SIOCATMARK：返回所有的「緊急」（帶外）資料是否都已被讀入。僅適用於 SOCK\_STREAM 類型的套介面，且該套介面已被設定為可以線上接收帶外資料（SO\_OOBINLINE）

### inet\_pton 與 inet\_ntop

inet\_pton 與 inet\_ntop 兩個函式，在 ASCII 字元描述的 IP 位址與網路位元組序的 4 位元組 IP 位址之間轉換。 字母 "n" 與 "p"，分別是 numerical 與 presentation 的縮寫。

## 協定和位址

插座 API 是 Unix 網路的通用介面，允許使用各種網路協定和位址。

下面列出了一些例子，在現在的 Linux 和 BSD 中一般都已經實現了。

```
PF_LOCAL, PF_UNIX, PF_FILE
                Local to host (pipes and file-domain)
PF_INET         IP protocol family
PF_AX25         Amateur Radio AX.25
PF_IPX          Novell Internet Protocol
PF_APPLETALK    Appletalk DDP
PF_NETROM       Amateur radio NetROM
PF_BRIDGE       Multiprotocol bridge
PF_ATMPVC       ATM PVCs
PF_X25          Reserved for X.25 project
PF_INET6        IP version 6
PF_ROSE         Amateur Radio X.25 PLP
PF_DECnet       Reserved for DECnet project
PF_NETBEUI      Reserved for 802.2LLC project
PF_SECURITY     Security callback pseudo AF
PF_KEY          PF_KEY key management API
PF_NETLINK, PF_ROUTE
                routing API
PF_PACKET       Packet family
PF_ASH          Ash
PF_ECONET       Acorn Econet
PF_ATMSVC       ATM SVCs
PF_SNA          Linux SNA Project
PF_IRDA         IRDA sockets
PF_PPPOX        PPPoX sockets
PF_WANPIPE      Wanpipe API sockets
PF_BLUETOOTH    Bluetooth sockets
```

socket 的通用 address 描述結構 sockaddr 是一個 16 位元組大小的結構（2+14），sa\_family 可以認為是 socket address family 的縮寫。另外的 14 位元組是用來描述位址。當指定 sa\_family=AF\_INET 之後，sa\_data 的形式也就被固定了下來：最前端的 2 位元組用於記錄 16 位元的埠，緊接著的 4 位元組用於記錄 32 位元的 IP 位址，最後的 8 位元組清空為零。

## 使用 TCP 的伺服器客戶機舉例

### 伺服器

設定一個簡單的 TCP 伺服器涉及下列步驟：

* 呼叫 socket 函式建立插座，應當使用的參數參見常式。
* 呼叫 bind 函式把插座繫結到一個監聽埠上。注意 bind 函式需要接受一個 sockaddr\_in 結構體作為參數，因此在呼叫 bind 函式之前，程式要先聲明一個 sockaddr\_in 結構體，用 memset 函式將其清零，然後將其中的 sin\_family 設定為 AF\_INET，接下來，程式需要設定其 sin\_port 成員變數，即監聽埠。需要說明的是，sin\_port 中的埠號需要以網路位元組序儲存，因此需要呼叫 htons 函式對埠號進行轉換（函式名是 "host to network short" 的縮寫）。
* 呼叫 listen 函式，使該插座成為一個處在監聽狀態的插座。
* 接下來，伺服器可以通過 accept 函式接受客戶端的連接請求。若沒有收到連接請求，accept 函式將不會返回並阻塞程式的執行。接收到連接請求後，accept 函式會為該連接返回一個插座描述符。accept 函式可以被多次呼叫來接受不同客戶端的連接請求，而且之前的連接仍處於監聽狀態 —— 直到其被關閉為止。
* 現在，伺服器可以通過對 send，recv 或者對 write，read 等函式的呼叫來同客戶端進行通信。
* 對於一個不再需要的插座，可以使用 close 函式關閉它。 Note that if there were any calls to `fork()`, each process must close the sockets it knew about (the kernel keeps track of how many processes have a descriptor open), and two processes should not use the same socket at once.

Python 實現：

### 客戶機

建立一個客戶機連接涉及以下步驟：

* 呼叫 `socket()` 建立插座。
* 用 `connect()` 連接到伺服器，類似伺服器端的操作，將一個 sin\_family 設為 AF\_INET,sin\_port 設為伺服器的監聽埠（依然要以網路位元組序）,sin\_addr 設為伺服器 IP 位址的（還是要用網路位元組序）的 sockaddr\_in 作為參數傳入。
* 用 `send()` 和 `recv()` 或者 `write()` 和 `read()` 進行通信。
* 用 `close()` 終止連接。如果呼叫 `fork()`, 每個行程都要用 `close()`。

Python 實現：

## 使用 UDP 的伺服器客戶機舉例

使用者資料報協定（UDP）是一個不保證正確傳輸的無連接協定。 UDP 封包可能會亂序到達，多次到達或者直接遺失。但是設計的負載比 TCP 小。

UDP 位址空間，也即是 UDP 埠，和 TCP 埠是沒有關係的。

### 伺服器

Code may set up a UDP server on port 7654 as follows:

上面的無限迴圈用 recvfrom() 接收給 UDP 埠 7654 的封包。使用如下參數：

* 指向快取資料指標
* 快取大小
* 標誌
* 位址
* 位址結構體大小

同樣功能的 Python 實現：

### 客戶機

用 UDP 封包傳送一個 "Hello World!" 給位址 127.0.0.1（迴環位址），埠 7654 。

`buffer` 指定要傳送資料的指標， `buffer_length` 指定快取內容的大小。

同樣功能的 Python 實現：

## 參見

* 電腦網路
* Winsock

## 參考資料

The "de jure" standard definition of the Sockets interface is contained in the POSIX standard, known as:

* IEEE Std. 1003.1-2001 Standard for Information Technology—Portable Operating System Interface (POSIX).
* Open Group Technical Standard: Base Specifications, Issue 6, December 2001.
* ISO/IEC 9945:2002

Information about this standard and ongoing work on it is available from the Austin website（頁面存檔備份，存於網際網路檔案館）.

The IPv6 extensions to the base socket API are documented in RFC 3493 and RFC 3542.

## 外部連結

* 手冊頁
  * accept (2)（頁面存檔備份，存於網際網路檔案館）
  * connect (2)（頁面存檔備份，存於網際網路檔案館）
* Beej's Guide to Network Programming（頁面存檔備份，存於網際網路檔案館） - 2007
  * Beej's Guide to Network Programming 正體中文版（zh-tw）（頁面存檔備份，存於網際網路檔案館） - 2014
  * Beej's Guide to Network Programming 簡體中文版（zh-cn）（頁面存檔備份，存於網際網路檔案館） - 2014
* UnixSocket FAQ（頁面存檔備份，存於網際網路檔案館）
* Get system IP list - C++ Example
* quick TCP-IP NetIntro with C examples（頁面存檔備份，存於網際網路檔案館）
* Porting Berkeley Socket programs to Winsock（頁面存檔備份，存於網際網路檔案館） - Microsoft's documentation.
* Programming UNIX Sockets in C - Frequently Asked Questions（頁面存檔備份，存於網際網路檔案館） - 1996
* Linux network programming（頁面存檔備份，存於網際網路檔案館） - _Linux Journal_, 1998

本條目部分或全部內容出自以 GFDL 授權發佈的《自由線上電腦詞典》（FOLDOC）。