Linux IPC之Socket網絡編程基礎篇

UNIX IPC工具使用總結 裏介紹了socket是一種用於通信的IPC工具。它允許位於同一主機跨主機上的應用程序之間交換數據。第一個被廣泛接受的socket API實現於1983年,出現在4.2BSD中,實際上這組API已經被移植到了所有UNIX實現以及大多數操作系統上。

概述

在一個典型的C/S場景中,應用程序使用socket進行通信的方式如下:

  • 各個應用程序創建一個socket。socket是一個允許通信的“設備”,兩個應用程序都需要用到它。
  • 服務器將自己的socket綁定到一個衆所周知的地址上,使得客戶端能夠定位到它的位置。
// 使用socket系統調用,能夠創建一個socket,它返回一個用來在後續系統調用中引用該socket的文件描述符
fd = socket(domain, type, protocol);

通信domain

socket存在於一個通信domain中,它確定:

  • 識別出一個socket的方法,即,socket“地址”的格式。
  • 通信範圍,即,在位於同一主機上的應用程序之間,還是位於跨主機的應用程序之間。

現代操作系統至少支持下列domain:

  • UNIX(AF_UNIX)domain
    允許在同一主機上的應用程序之間進行通信。

POSIX.1g使用AF_LOCAL作爲AF_UNIX的同義詞,但是SUSv3並沒有使用這個名稱。

  • IPv4(AF_INET)domain
    允許在使用IPv4網絡連接起來的主機上的應用程序之間進行通信。

  • IPv6(AF_INET6)domain
    允許在使用IPv6網絡連接起來的主機上的應用程序之間進行通信。

儘管IPv6被設計成了IPv4的接任者,但目前後一種協議仍然是使用最廣的協議。

總結

domain 執行的通信 應用程序間的通信 地址格式 地址結構
AF_UNIX 內核中 同一主機 路徑名 sockaddr_un
AF_INET IPv4 IPv4連接起來的主機 32位IPv4地址 + 16位端口號 sockaddr_in
AF_INET6 IPv6 IPv6連接起來的主機 128位IPv6地址 + 16位端口號 sockaddr_in6

socket類型

每個socket實現都至少提供了兩種socket流和數據報。這兩種socket類型在UNIX和Internet domain中都得到了支持。

屬性 數據報
可靠的傳遞? YES NO
消息邊界保留? NO YES
面向連接? YES NO

流socket(SOCK_STREAM)

流socket提供了一個可靠的雙向的字節流通信信道

  • 可靠的
    表示可以保證發送者傳輸的數據會完整無缺地到達接收應用程序(假設網絡連接和接收者都不會崩潰),或收到一個傳輸失敗的通知。

  • 雙向的
    表示數據可以在兩個socket之間的任意方向上傳輸。

  • 字節流
    表示與管道一樣不存在消息邊界的概念。

一個流socket類似於使用一對允許在兩個應用程序之間進行雙向通信的管道,它們之間的差別在於socket(Internet domain)允在在網絡上進行通信。

數據報socket(SOCK_DGRAM)

數據報socket允許數據以被稱爲數據報的消息的形式進行交換。在數據報socket中,消息邊界得到了保留,但是數據傳輸是不可靠的,消息的到達可能是無序的,重複的,或者根本就無法達到。

數據報socket是更一般的無連接socket。與流socket不同,一個數據報socket在使用時無需與另一個socket連接。
注意:數據報socket可以與另一個socket連接,但其語義與連接的流socket是不同的。

對比

在Internet domain中:

類型 協議 名稱
流socket 傳輸控制協議TCP TCP socket
數據報socket 數據報協議UDP UDP socket

socket系統調用

關鍵的socket系統調用包括:

系統調用 描述
socket() 創建一個新的socket
bind() 將一個socket綁定到一個地址上。通常,服務器需要使用這個調用來將其socket綁定到一個衆所周知的地址上,使得客戶端能夠定位到該socket上。
listen() 允許一個流socket接收來自其他socket的接入連接。
accept() 在一個監聽socket上接收來自一個對等應用程序的連接,並可選地返回對等socket的地址。
connet() 建立與另一個socket之間的連接

在大多數Linux架構上(除了Alpha和IA-64),所有這些socket系統調用實際上被實現成了通過單個系統調用socketcall()進行多路複用的庫函數(最初的時候)。但是,上述函數都被稱爲系統調用,是因爲它們在最初的BSD實現,以及其他很多同時代的UNIX實現上,是被作爲系統調用實現的。

socket I/O可以使用傳統的read()write()系統調用使用,或使用一組socket特有的系統調用,如,send()recv()sendto()recvfrom()來完成。

注意:在默認情況下,這些系統調用在I/O操作無法被立即完成時會阻塞。通過使用fcntl()F_SETFL操作來啓用O_NONBLOCK打開文件狀態標記可以執行非阻塞I/O。

流socket注意事項

流socket的運作與電話系統類似。具體流程可以參考TCP連接建立和終止及TCP狀態轉換 。在大多數使用流socket的應用程序中,服務器會執行被動打開,而客戶端會執行主動式打開。

pic

#include <sys/socket.h>

// return file descriptor on success, or -1 on error
int socket(int domain, int type, int protocol);

// return 0 on success, or -1 on error
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 通用的地址結構(所有domain特定的地址結構模版),用途是,將各種domain特定的地址結構轉換成單個類型,以供socket系統調用中的各個參數適用
struct sockaddr {
    sa_family_t sa_family;   /* Address family (AF_* constant) */
    char        sa_data[14]; /* Socket address (size varies according to socket domain) */
}

// return 0 on success, or -1 on error
int listen(int sockfd, int backlog);

// return file descriptor on success, or -1 on error
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

// return 0 on success, or -1 on error
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • socket()接口中,protocol參數通常爲0,在裸socket(SOCK_RAW)中,會將protocol指定爲IPPROTO_RAW。

從內核2.6.27開始,Linux爲type參數提供了第二種用途,即允許兩個非標準的標記與socket類型取OR。

  • 除了將一個服務器的socket綁定到一個衆所周知的地址,對於一個Internet domain socket來講,服務器可以不調用bind()而直接調用listen(),這將會導致內核爲該socket選擇一個臨時端口。之後服務器可以使用getsockname()來獲取socket的地址。

  • 無法在一個已連接的socket(即,已經成功執行connect()的socket,或由accept()返回的socket)上執行listen()。

  • 理解backlog參數的用途。

  1. 客戶端可能會在服務器調用accept()之前調用connect(),這將會產生一個未決的連接。內核必須要記錄所有未決的連接請求的相關信息,這樣後續的accept()就能夠處理這些請求。backlog參數允許限制這種未決連接的數量。在這個限制內的連接請求會立即成功,之外的連接請求就會阻塞直到一個未決的連接被accept()接受,並從未決連接隊列刪除爲止。

  2. SUSv3規定實現應該通過在<sys/socket.h>中定義SOMAXCONN常量來發布這個限制。在Linux上,這個常量的值被定義爲128。但是,從2.4.25內核起,Linux允許在運行時通過/proc/sys/net/core/somaxconn文件來調整這個限制。

  3. 在最初的BSD socket實現中,backlog的上限是5,並且在較早的代碼中可以看到這個數值。但是,所有現代實現允許爲backlog指定更高的值,這對於使用TCP socket服務大量客戶的網絡服務器來講是由必要的。

pic

  • accept()通過文件描述符sockfd監聽流socket上接受一個接入連接,如果在調用accept()時不存在未決的連接,那麼調用就會阻塞直到有連接請求到達爲止。傳入accept()的剩餘參數會返回對端socket的地址。如果不關心對等socket的地址,可以將addr和addrlen分別指定爲NULL和0(後續可以通過getpeername())來獲取對端的地址。

理解accept的關鍵:它會創建一個新socket,並且正是這個新socket會與執行connect()的對等socket進行連接。accept()返回的函數結果是已連接的socket的文件描述符。
從內核2.6.28開始,Linux支持一個新的非標準系統調用accept4()。這個系統調用執行的任務與accept()相同,但是,支持一個額外的參數flags,而這個參數可以用來改變系統調用的行爲。目前系統支持兩個標記:SOCK_CLOEXECSOCK_NONBLOCK

  • 如果connect()失敗並且希望重新進行連接,那麼SUSv3規定完成這個任務的可移植的方法是關閉這個socket,創建一個新socket,在該新socket上重新進行連接。

數據報socket注意事項

數據報socket的運作類似於郵政系統。

pic

// return number of bytes received, 0 on EOF, or -1 on error
ssize_t recvfrom(int sockfd, void *buffer, size_t length, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

// return number of bytes sent, or -1 on error
ssize_t sendto(int sockfd, const void *buffer, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • 不管length的參數值是什麼,recvfrom()只會從一個數據報socket中讀取一條消息。如果消息的大小超過了length字節,那麼消息會被靜默地截斷爲length字節。

  • 在Linux上可以使用sendto()發送長度爲0的數據報,但不是所有的UNIX實現都允許這樣做。

  • 儘管數據報socket是無連接的,但在數據報socket上應用connect()仍然是有效的。在數據報socket上調用connect()會導致內核記錄這個socket()的對等socket的地址。

當一個數據報socket已連接之後:
1. 數據報的發送可在socket上使用write()或send()來完成,與sendto()一樣,每個write()會發送一個獨立的數據報。
2. 在這個socket上只能讀取由對等socket發送的數據報。

  • 注意,connect()的作用對數據報socket是不對稱的。通過再發起一個connect()可以修改一個已連接的數據報socket的對等socket。

總結

爲了一個數據報socket設置一個對等socket,這種做法的一個明顯優勢是在該socket上傳輸數據時可以使用更簡單的I/O系統調用,即,無需使用指定了dest_addr和addrlen參數的sendto(),而只需要使用write()即可。設置一個對等socket主要對那些需要單個對等socket發送多個數據報的應用程序是比較有用的。

在一些TCP/IP實踐中,將一個數據報socket連接到一個對等socket能夠帶來性能上的提升。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章