伯克利套接字(Berkeley sockets),也稱爲BSD Socket。伯克利套接字的應用編程接口(API)是採用C語言的進程間通信的庫,經常用在計算機網絡間的通信。 BSD Socket的應用編程接口已經是網絡套接字的抽象標準。大多數其他程序語言使用一種相似的編程接口。它最初是由加州伯克利大學爲Unix系統開發出來的。所有現代的操作系統都實現了伯克利套接字接口,因爲它已經是連接互聯網的標準接口了。
API函數
以下函數是最基本的 socket API
socket()
創造某種類型的套接字,分配一些系統資源,用返回的整數識別。bind()
一般是用在服務器這邊,和一個套接字地址結構相連,比如說是一個特定的本地端口號和一個IP地址。listen()
用在服務器一邊,導致一個綁定的TCP套接字進入監聽狀態。connect()
用在客戶機這邊,給套接字分配一個空閒的端口號。比如說一個TCP套接字,它會試圖建立一個新的TCP連接。accept()
用在服務器這邊。從客戶機那接受請求試圖創造一個新的TCP連接,並把一個套接字和這個連接相聯繫起來。send()
andrecv()
, orwrite()
andread()
, orsendto()
andrecvfrom()
用來接收和發送數據。close()
當套接字的引用計數爲0的時候纔會引發TCP的四次揮手,關閉連接,系統釋放資源。------- 不可對某個socket連續調用兩次close,否則第二次調用會出現釋放未分配的內存問題(野指針)(在LWIP下測試得出的結論)。個人想法:應該在close函數裏面把socket置成某個數,這樣每次進入close,如果socket等於某個數,表示已經close過,直接函數返回。- shutdown() 不用管套接字的引用計數,調用讀寫函數返回小於0的數,退出阻塞。
以下函數用來設置 / 獲取套接字的屬性,有些函數的功能有重疊;fcntl() 和 ioctl() 功能比較強大,在linux c 中不僅僅用來網絡編程
gethostbyname()
andgethostbyaddr()
用來解析主機名和地址。select()
------ 用來獲取指定套接字的狀態(可讀、可寫或者出錯)- fcntl() ------ 設置套接字的工作模式(阻塞或非阻塞)
- ioctl()
poll()
is used to check on the state of a socket in a set of sockets. The set can be tested to see if any socket can be written to, read from or if an error occurred.getsockopt()
------ 獲得套接字的屬性,有些屬性的開關在 opt.h 和 lwipopts.hsetsockopt()
------ 設置套接字的屬性,有些屬性的開關在 opt.h 和 lwipopts.h- getsockname()、getpeername() ------ 用於獲取本地、對方IP和端口號
以下函數用來數據的轉換
- ntohs()、ntohl()、htons()、htonl() ------ 大端編碼和小端編碼的轉換,s表示short(16bit),l表示long(32bit);h 代表 host,就是本地主機的表示形式; n 代表 network,表示網絡上傳輸的字節序
- inet_ntoa() 、inet_addr() ----- inet_ntoa()將一個 32bits 無符號整數轉換爲點分十進制IP格式的字符串,inet_addr()正好相反
- inet_pton() ---- inet_pton() 的功能和 inet_addr() 功能類似
- IP4_ADDR --- 把 IP地址中的4個數字整合放到 u32 類型的變量
更多的函數說明如下:
recv、send和read、write
這四個函數用在 TCP 通信中
recv和send函數提供了和read和write差不多的功能.不過它們提供了第四個參數來控制讀寫操作
int recv(int sockfd,void *buf,int len,int flags) int send(int sockfd,void *buf,int len,int flags)
對於send(),flags取值有: 0: 與write()無異,阻塞操作 MSG_DONTROUTE:告訴內核,目標主機在本地網絡,不用查路由表 MSG_DONTWAIT:將單個I/O操作設置爲非阻塞模式 MSG_OOB:指明發送的是帶外信息 對於recv(),flags取值有: 0:與read()無異,阻塞操作 MSG_DONTWAIT:將單個I/O操作設置爲非阻塞模式 MSG_OOB:指明發送的是帶外信息 MSG_PEEK:表示只是從系統緩衝區中讀取內容,而不清除系統緩衝區的內容.這樣下次讀的時候,仍然是一樣的內容.一般在有多個進程讀寫數據時可以使用這個標誌. MSG_WAITALL:通知內核直到讀到請求的數據字節數時,或者發送了錯誤才返回。 對於接收數據函數的返回值: 大於0:讀到數據的字節數 等於0:套接字關閉,讀到FIN 小於0: EINTR:表示操作被中斷,下次可以繼續讀取,忽視這個返回值 EWOULDBLOCK(EAGAIN):當非阻塞讀取時,沒有數據可讀,返回此操作碼 ECONNRESET:對方發送RST ...... 對於發送數據函數的返回值: 大於0:成功發送數據的字節數 等於0:套接字關閉 小於0: EINTR:表示連接正常,操作被中斷,此次數據發送失敗,下次可以繼續發送 EWOULDBLOCK(EAGAIN):表示連接正常,但發送緩衝區沒有空間,此次數據發送失敗,下次可以繼續發送 ...... 注:不能通過 shutdown 讀寫通道主動退出服務器的阻塞read(),但是可以主動退出退出客戶端的阻塞read(),ESP8266測試得到
recvfrom、sendto
這兩個函數用在UDP通信的接收和發送
int recefrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen)
int sendto(int sockfd, const void *buf, size_t size, int flags, const struct sockaddr *to, socklen_t tolen)
socket()
函數原型:
int socket(int domain, int type, int protocol);
socket()
爲通信創造一個端點並返回一個文件描述符。 socket()
有三個參數:
- domain,確定地址協議。例如:
PF_INET
(AF_INET)是IPv4PF_INET6
是 IPv6.
- type,是下面中的一個:
SOCK_STREAM
(Stream Socket)SOCK_DGRAM
(Datagram Socket)SOCK_RAW
(Raw Socket)。
- protocol, 規定套接字發送和接送哪類型協議數據。最常見的是
IPPROTO_TCP
,IPPROTO_UDP
,IPPROTO_UDPLITE、IPPROTO_ICMP
。如果domain
和type
已經確定唯一的協議,“0(
IPPROTO_IP
)
” 可以用來表示選擇一個默認的協議。
例子:
tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_IP); // 由於 PF_INET 和 SOCK_STREAM 已經可以確認是使用 IPPROTO_TCP,所以第三個參數填什麼都不影響 udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_IP); // PF_INET 和 SOCK_DGRAM 表明使用 IPPROTO_UDP 或 IPPROTO_UDPLITE 其中一種協議,第三個參數填的不是 IPPROTO_UDPLITE 都是IPPROTO_UDP
如果出錯返回-1,否則返回一個代表文件描述符的整數(一般有個宏NUM_SOCKETS規定一共可以創建多少個socket,所以socket()創建成功返回的整數範圍爲0~NUM_SOCKETS-1,而且是從小到大返回)
bind()
bind()
給套接字分配一個地址。當使用 socket()
創造一個套接字時, 只是給定了協議族,並沒有分配地址。在套接字能夠接受來自其他主機的連接前,必須用bind()給它綁定一個地址。 bind()
由三個參數:
sockfd
, 代表socket的文件描述符。my_addr
, 指向sockaddr
結構體的指針,代表要綁定的地址 。addrlen
, 是sockaddr結構體的大小。
Bind()返回0表示成功,錯誤返回-1。
函數原型:
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
listen()
一旦一個套接字和一個地址聯繫之後,listen()
監聽到來的連接。但是這隻適用於對面向連接的模式,例如 套接字類型是 (SOCK_STREAM
, SOCK_SEQPACKET
)。listen()需要兩個參數:
sockfd:
一個有效的套接字描述符。backlog:accept 隊列大小。當服務器接收到第三次握手後將連接放入這個隊列中,直到被 accept 處理才清除,當 accept 隊列滿了之後,即使 client 繼續向 server 發送 ACK 包,也不會被響應,此時 ListenOverflows+1,同時 server 通過 /proc/sys/net/ipv4/tcp_abort_on_overflow(linux kernel 2.2 之後)來決定如何返回,0表示直接丟棄該 ACK,1表示發送 RST 通知 client;相應的,client 則會分別返回 read timeout 或者 connection reset by peer。參考下圖。
一旦連接被接受,返回0表示成功,錯誤返回-1。
函數原型:
int listen(int sockfd, int backlog);
accept()
當應用程序監聽來自其他主機的面對數據流的連接時,通過事件(比如Unix select()系統調用)通知它。必須用accept()
函數初始化連接。 Accept() 爲每個連接創立新的套接字並從監聽隊列中移除這個連接。它使用如下參數:
sockfd
,監聽的套接字描述符cliaddr
, 指向sockaddr 結構體的指針,客戶機地址信息。addrlen
,指向socklen_t
的指針,確定客戶機地址結構體的大小 。
返回新的套接字描述符,出錯返回-1。和客戶端的通信是通過這個套接字。假如是 sockfd(服務器套接字,accept 第一個參數) 0,accpet 返回的是 1,遞增。可以在沒關閉 accept 返回的套接字之前關閉 accept 的套接字,這樣防止新的客戶端連上。但是有個問題,如果關閉服務器套接字,accept 返回一個套接字是1,用 select 函數的第一個參數填套接字總數,現在的總是是1,而唯一個套接字的值是1(不是0),所以 select 會返回小於0,不能正常使用 select 函數。
Datagram 套接字不要求用accept()處理,因爲接收方可能用監聽套接字立即處理這個請求。
函數原型:
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
注:不能通過 close() 或者 shutdown() 主動退出 accept() 阻塞,ESP8266測試得出。
connect()
connect()
系統調用爲一個套接字設置連接,參數有文件描述符和主機地址。
函數原型:
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
close()
close()函數只是將這個套接字引用計數減1,就像rm一樣,刪除一個文件時只是移除一個硬鏈接數,只有這個套接字的所有引用計數減到0,套接字描述符纔會真的被關閉,纔會開始後續的四次揮手。
關於引用計數:操作系統創建子進程時,子進程將繼承父進程打開的套接字,父子進程擁有對該套接字同樣的訪問權,系統對每個套接字的引用進行計數,每增加一個進程訪問套接字,計數加1,當進程完成對套接字的使用時,對套接字的使用調用 close 進行關閉,close 調用將減少套接字的引用計數,並且在計數值爲0時刪除該套接字
SO_LINGER選項對close()的行爲有影響
SO_LINGER 的 I_onoff 爲0(默認情況):
在套接字上不能再發出發送或接收請求,套接字發送緩衝區中的內容被髮送到對端,如果描述符引用計數變爲0,在發送完發送緩衝區的數據後,後跟正常的TCP連接終止序列(即發送FIN),套接字接收緩衝區中的內容被丟棄
SO_LINGER 的 l_onoff = 1,l_linger = 0:
在套接字上不能再發出發送或接收請求,如果描述符引用計數變爲0,RST被髮送到對端,連接的狀態被置爲CLOSED(沒有TIME_WAIT狀態),套接字發送緩衝區和接收緩衝區中的內容被丟棄;
shutdown()
可以指定關閉讀通道、寫通道或者讀寫通道。
ESP8266 LWIP TCP客戶端情況下(不推薦用shutdown退出阻塞,推薦用select()):
調用了shutdown()的讀通道,不會引發四次揮手,但是執行read()會退出阻塞,返回小於0的數,然後再close()引發四次揮手,釋放資源。
特別強調:執行 shutdown() 後執行 close(),然後再執行read()會阻塞,永遠的阻塞。。。
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 *。
這些函數並不是伯克利套接字嚴格的組成部分。這些函數可能是過時了,新函數是 getaddrinfo() and getnameinfo(), 這些新函數是基於addrinfo數據結構。
函數原型:
struct hostent *gethostbyname(const char *name); struct hostent *gethostbyaddr(const void *addr, int len, int type);
setsockopt()、getsockopt()
int setsockopt( int socket, int level, int option_name, const void *option_value, size_t option_len);
sockfd: 套接字
level: 協議層(SOL_SOCKET、IPPROTO_IP、IPPRO_TCP)
opt_name: 選項名,每一個協議層都有其一些固定的選項名,
option_value: 緩衝區,setsockopt() 是指向將要存放的地址,getsockopt() 是指向目前存放信息的地址
option_len: 緩衝區大小長度
在 socket.h 有 level 和 opt_name 的介紹
SOL_SOCKET層,有如下選項:
opt_name | 說明 | option_value類型 |
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 |
...... |
IPPROTO_TCP層,有如下選型:
TCP_NODELAY
TCP_KEEPIDLE(設置多久沒接收到數據開始發送keepalive包)
TCP_KEEPINTVL(設置每個keepalive包的間隔時間)
TCP_KEEPCNT(設置發送多少個keepalive包)
......
ntohs()、ntohl()、htons()、htonl()
ntohs(n) //n爲16位數據類型,網絡字節順序到主機字節順序的轉換
htons(n) //n爲16位數據類型,主機字節順序到網絡字節順序的轉換
ntohl(n) //n爲32位數據類型,網絡字節順序到主機字節順序的轉換
htonl(n) //n爲32位數據類型,主機字節順序到網絡字節順序的轉換
網絡字節順序採用大端模式進行編址,而主機字節順序根據處理器的不同而不同,如PowerPC處理器使用大端模式,而Pentuim處理器使用小端模式。大端模式處理器的字節序到網絡字節序不需要轉換,此時ntohs(n)=n,ntohl(n) = n;而小端模式處理器的字節序到網絡字節必須要進行轉換
select()
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout)
本函數用於確定一個或多個套接口的狀態。對每一個套接口,調用者可查詢它的可讀性、可寫性及錯誤狀態信息。用結構體 fd_set 來表示一組等待檢查的套接口。在調用返回時,這個結構存有滿足一定條件的套接口組的子集,並且select()返回滿足條件的套接口的數目,0表示超時,-1表示出錯。有一組宏可用於對 fd_set 的操作(宏的功能有:把套接口放入 fd_set 結構、清除 fd_set 結構中的某個套接口、檢查否個套接口是否可讀可寫等)
maxfdp1:等於創建成功的socket個數,socket()成功返回的整數範圍爲0~NUM_SOCKETS-1,NUM_SOCKETS爲最多能創建的個數,所以maxfdp1一般取值爲socket()返回的最大值+1
readset:(可選)指針,指向一組等待可讀性檢查的套接口。如果該套接口正處於監聽listen()狀態,則若有連接請求到達,該套接口便被標識爲可讀,這樣一個accept()調用保證可以無阻塞完成。對其他套接口而言,可讀性意味着有數據供讀取,於是recv()或recvfrom()操作均能無阻塞完成
writeset:(可選)指針,指向一組等待可寫性檢查的套接口。如果一個套接口正在connect()連接(非阻塞),可寫性意味着連接順利建立。如果套接口並未處於connect()調用中,可寫性意味着send()和sendto()調用將無阻塞完成
exceptset:(可選)指針,指向一組等待錯誤檢查的套接口。
timeout:select()最多等待時間,對阻塞操作則爲NULL
不需要查看的形參可以設爲NULL
返回值:負值:select錯誤,正值:可讀可寫套接口數目,0:等待超時,沒有可讀寫或錯誤的文件
判讀:通過宏 FD_ISSET、&readset(宏參數) 和 具體某個描述符(宏參數) 判斷該描述符是否可讀;通過宏 FD_ISSET、&writeset(宏參數) 和 具體某個描述符(宏參數) 判斷該描述符是否可寫;
fcntl()
int fcntl(int s, int cmd, int val);
s 爲要設置的socket,cmd 是要進行什麼操作,val 是操作所需要的參數
返回值:成功取決於cmd,失敗返回-1
函數有下面5種功能:
- 複製一個現有的描述符(cmd=F_DUPFD);
- 獲得/設置文件描述符標記(cmd=F_GETFD或F_SETFD);
- 獲得/設置文件狀態標記(cmd=F_GETFL或F_SETFL);
- 獲得/設置異步I/O所有權(cmd=F_GETOWN或F_SETOWN);
- 獲得/設置記錄鎖(cmd=F_GETLK , F_SETLK或F_SETLKW);
在socket編程中使用 fcntl() 設置 sockfd 爲非阻塞模式,則之後的connect、accept、recv、recvfrom等函數便失去了阻塞功能,變成了非阻塞函數。
用以下方法將socket設置爲非阻塞方式:
int flags = fcntl(socket, F_GETFL, 0); fcntl(socket, F_SETFL, flags | O_NONBLOCK);
將非阻塞的設置回阻塞可以用以下方式:
int flags = fcntl(socket, F_GETFL, 0); fcntl(socket, F_SETFL, flags & ~O_NONBLOCK);
ioctl()
int ioctl(int s, long request, void *argp);
ioctl 是 input output control 的簡寫,控制I/O設備, 提供了一種獲得設備信息和向設備發送控制參數的手段
返回值:成功返回0,出錯返回-1
第一個參數:指示某個文件描述符(當然也包括 套接口描述符)
第二個參數:request 指示要ioctl執行的操作
第三個參數:總是某種指針,具體的指向類型依賴於 request 參數
我們可以把和網絡相關的請求(request)劃分爲6 類:
套接口操作
文件操作
接口操作
ARP 高速緩存操作
路由表操作
流系統
下表列出了網絡相關ioctl 請求的request 參數以及arg 地址必須指向的數據類型:
類別 |
Request |
說明 |
數據類型 |
套 |
SIOCATMARK |
是否位於帶外標記 |
int |
文 |
FIONBIN |
設置/ 清除非阻塞I/O 標誌 |
int |
接 |
SIOCGIFCONF |
獲取所有接口的清單 |
struct ifconf |
ARP |
SIOCSARP |
創建/ 修改ARP 表項 |
struct arpreq |
路 |
SIOCADDRT |
增加路徑 |
struct rtentry |
流 |
I_xxx |
|
|
inet_ntoa()、inet_addr()
inet_ntoa()將一個 32bits 無符號整數轉換爲點分十進制IP格式的字符串。inet_addr()正好相反,inet_addr可以判斷形參是否IP格式的字符串,不是返回0xFFFFFFFF
char* inet_ntoa (struct in_addr addr);
結構體in_addr唯一一個成員就是32bits 無符號整數
inet_pton()
int inet_pton(int af, const char *src, void *dst);
af 是地址簇,比如 af=AF_INET,AF_INET(又稱 PF_INET)是 IPv4 網絡協議的套接字類型
指針 src 指向十進制IP格式的字符串
指針 dst 指向 32bits 無符號整數,即要得到的數據
IP4_ADDR()
IP4_ADDR(ipaddr, a,b,c,d)
ipaddr是一個結構體(struct ip_addr),含有成員變量addr
宏定義
PF_INET(AF_INET):IPv4(網際層)
PF_INET6(AF_INET6):IPv6(網際層)
SOCK_STREAM:設置套接字類型爲流套接字,流套接字協議只有一個,即 IPPROTO_TCP(傳輸層)
SOCK_DGRAM:設置套接字類型爲數據報套接字,數據報套接字協議有兩個個,即 IPPROTO_UDP 和 IPPROTO_UDPLITE(傳輸層)
SOCK_RAW:設置套接字類型爲原始套接字
原始套接字(SOCKET_RAW)允許對較低層次的協議直接訪問,比如IP、 ICMP協議,它常用於檢驗新的協議實現,或者訪問現有服務中配置的新設備,因爲RAW SOCKET能夠對網絡底層的傳輸機制進行控制,所以可以應用原始套接字來操縱網絡層和傳輸層應用。比如,我們可以通過RAW SOCKET來接收發向本機的ICMP、IGMP協議包,或者接收TCP/IP棧不能夠處理的IP包,也可以用來發送一些自定包頭或自定協議的IP包。網絡監聽技術很大程度上依賴於SOCKET_RAW
套接字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
標籤: 網絡通信知識