第一章 TCP網絡程式設計
socket建立套接字
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); |
功能
建立網絡套接字,用于網絡通信使用,類似于檔案操作的open函數。該函數在伺服器和用戶端都會用到。
參數
-
int domain :網絡協定版本指定。AF_INET IPv4 Internet protocols
AF_INET6 IPv6 Internet protocols
-
int type:指定通信協定類型。SOCK_STREAM 表明我們用的是TCP協定 (位元組流)
SOCK_DGRAM 表明我們用的是UDP協定 (資料報)
- int protocol:指定通信協定類型。Type參數已經指定了協定,該參數直接填0即可!

圖1-1
傳回值
成功傳回網絡套接字,與open函數傳回值類似。
示例
Clientfd = socket(PF_INET,SOCK_STREAM,0); |
bind綁定IP-端口
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); |
功能
建立伺服器。該函數在伺服器端使用。
參數
- int sockfd : 網絡套接字
- const struct sockaddr *addr :填充建立伺服器所需的位址資訊,詳細的成員看1.3章節。
- socklen_t addrlen :位址長度,就是該結構體的大小。使用sizeof函數進行計算。
傳回值
0表示成功,-1表示失敗!
struct sockaddr位址結構體
1.3.1 結構體成員解析
在實際填充參數的過程中,struct sockaddr結構體被struct sockaddr_in結構體代替。struct sockaddr_in結構體比struct sockaddr可讀性強一些,填充參數比較好了解。
struct sockaddr_in和struct sockaddr大小相同。在填充結構體的時候為了友善填充參數,使用struct sockaddr_in結構體,給函數指派的時候需要強制轉換為struct sockaddr類型的結構體。因為底層函數最終還是使用struct sockaddr類型的結構體。
- struct sockaddr結構體成員:
struct sockaddr { sa_family_t sa_family; //網絡協定版本。填寫:AF_INET 或者 AF_INET6。 char sa_data[14]; //IP位址和端口 } |
- struct sockaddr_in結構體成員:檢視IPV4協定幫助文檔:# man 7 ip
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET 協定類型*/ in_port_t sin_port; /* port in network byte order 端口号*/ struct in_addr sin_addr; /* internet address 存放IP位址的結構體*/ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order IP位址 */ }; |
1.3.2 端口号指派
計算機資料存儲有兩種位元組優先順序:高位位元組優先和低位位元組優先。 Internet 上資料以高位位元組優先順序在網絡上傳輸, 是以對于在内部是以低位位元組優先方式存儲資料的機器, 在 Internet 上傳輸資料時就需要進行轉換, 否則就會出現資料不一緻。
普通人用的桌面電腦,隻要是Intel或AMD的x86/x64架構就一定是小端位元組序。
外很多ARM CPU可以選擇資料指令位元組序,不過通常也都是運作小端位元組序(比如我們的智能手機)。
網絡裝置,像PowerPC核心的一些路由器,預設運作大端位元組序。
下面是幾個位元組順序轉換函數:
·htonl(): 把 32 位值從主機位元組序轉換成網絡位元組序
·htons(): 把 16 位值從主機位元組序轉換成網絡位元組序
·ntohl(): 把 32 位值從網絡位元組序轉換成主機位元組序
·ntohs(): 把 16 位值從網絡位元組序轉換成主機位元組序
函數原型
#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); 網際協定在處理這些多位元組整數時,使用大端位元組序。 在主機本身就使用大端位元組序時,這些函數通常被定義為空宏。 |
給struct sockaddr_in結構體的端口成員指派的時候就需要用到以上大端轉小端函數進行轉換!
示例:
/*結構體成員指派*/ tcp_server.sin_family=AF_INET; //IPV4協定類型 tcp_server.sin_port=htons(tcp_server_port);//端口号指派,将本地位元組序轉為網絡位元組序 tcp_server.sin_addr.s_addr=INADDR_ANY; //将本地IP位址指派給結構體成員 //inet_addr("192.168.18.3"); //IP位址指派 |
1.3.3 IP位址指派
struct sockaddr_in結構體存放IP位址的成員是struct in_addr 結構體類型,底層存放位址的成員是一個無符号int類型,而我們生活中的IP位址是使用xxx.xxx.xxx.xxx 這種格式表示的。比如:192.168.1.1。 在指派的時候就需要進行将”192.168.1.1”這種格式轉為無符号int類型才能進行指派。
以下是幾個IP格式轉換函數:
- 将字元串類型IP轉為in_addr_t類型(unsigned int)傳回。
in_addr_t inet_addr(const char *cp); |
示例:
Serveraddr.sin_addr.s_addr = inet_addr("192.168.18.3");
- 使用字元串類型的IP直接給結構體成員指派
int inet_aton(const char *cp, struct in_addr *inp); |
示例:
inet_aton(“192.168.18.3”,&Clientaddr.sin_addr);
- 将結構體裡的IP位址成員轉為字元串類型傳回
char *inet_ntoa(struct in_addr in); |
該函數與上面兩個函數功能剛好相反。是将整型的IP轉為字元串類型!
1.3.4 本地計算機大小端判斷
首先說明,電腦大小端指的是一種存儲模式。
1.為什麼有大小端:
在計算機系統中,我們是以位元組為機關的,每個位址單元都對應着一個位元組,一個位元組為 8bit。但是在C語言中除了8bit的char之外,還有16bit的short型,32bit的long型(要看具體的編譯器),另外,對于位數大于 8位的處理器,例如16位或者32位的處理器,由于寄存器寬度大于一個位元組,那麼必然存在着一個如何将多個位元組安排的問題,是以就導緻了大端存儲模式和小端存儲模式。
2.大小端定義:
大端模式(Big-endian),是指資料的高位元組,儲存在記憶體的低位址中,而資料的低位元組,儲存在記憶體的高位址中。
小端模式(Little-endian),是指資料的高位元組儲存在記憶體的高位址中,而資料的低位元組儲存在記憶體的低位址中。
3.直接來看一個圖,詳細說明大小端:
例子:int i = 0x12345678 兩種模式存入記憶體:
4. 判斷大小端的C語言代碼
#include<stdio.h> int CheckSystem() { unioncheck { int i; char ch; }c; c.i=1; return (c.ch==1); } int main() { int check=CheckSystem(); if(check==1) printf("目前系統為小端\n"); else printf("目前系統為大端\n"); return 0; } /////////////////////////////////////////////////////////////// // 公用的四個位元組位址:0x1001 -> 0x1002 -> 0x1003 -> 0x1004 // 小端來說指派 1 : 0x01 0x00 0x00 0x00 // 大端來說指派 1 : 0x00 0x00 0x00 0x01 //也就是說存資料都是從低位址存放一個char位元組, //他和int開始的位址是一樣的讀的話還是從低位元組向高位元組完整的讀取 |
listen監聽端口的數量
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); |
功能
設定伺服器需要監聽的端口數量。決定了能夠連接配接的伺服器數量。
傳回值
成功傳回0,失敗傳回-1。
伺服器建立,函數調用順序:
圖1-2
- 示例:listen(Serverfd,10);
accept 等待用戶端連接配接
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
功能
以阻塞的形式等待用戶端連接配接。
參數
struct sockaddr *addr :存放已經連接配接的用戶端資訊。傳入一個結構體位址。
socklen_t *addrlen :表示用戶端的結構體大小。該大小需要我們指定,用戶端連接配接成功然後再判斷是否與填寫的大小一緻。
傳回值
成功将傳回用戶端的網絡套接字。錯誤傳回-1。
- 示例:
struct sockaddr_in Clientaddr; len = sizeof(struct sockaddr); Clientfd = accept(Serverfd,(struct sockaddr *)&Clientaddr,&len); |
connect連接配接伺服器
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); |
功能
連接配接到指定伺服器。該函數在用戶端使用。
參數
int sockfd :socket函數的網絡套接字。
const struct sockaddr *addr :伺服器的IP位址資訊。 參考:1.2節和1.3.節
socklen_t addrlen :結構體的大小。
傳回值
成功傳回0,錯誤傳回-1。
- 示例
connect(Clientfd,(struct sockaddr *)&Clientaddr,sizeof(struct sockaddr)); |
send/ recv網絡資料收發
#include <sys/types.h> #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv (int sockfd, void *buf, size_t len, int flags); |
功能
用戶端與伺服器之間的資料收發。
參數
const void *buf 、void *buf :讀寫的緩沖區。
int flags :填0。
- 以上兩個函數可以使用write和read函數替換。
shutdown關閉連接配接
#include <sys/socket.h> int shutdown(int sockfd, int how); |
傳回
0—成功,-1—失敗。
參數how的值:
SHUT_RD:關閉連接配接的讀這一半,不再接收套接口中的資料且留在套接口緩沖區中的資料都廢棄。程序不能再對套接口任何讀函數。調用此函數後,由TCP套接口接收的任何資料都被确認,但資料本身被扔掉。
SHUT_WR:關閉連接配接的寫這一半,在TCP場合下,這稱為半關閉。目前留在套接口發送緩沖區中的資料都被發送,後跟正常的TCP連接配接終止序列。此半關閉不管套接口描述字的通路計數是否大于0。程序不能再執行對套接口的任何寫函數。
SHUT_RDWR:連接配接的讀這一半和寫這一半都關閉。這等效于調用shutdown兩次:第一次調用時用SHUT_RD,第二次調用時用SHUT_WR。
shutdown(tcp_client_fd,SHUT_WR); //TCP半關閉,保證緩沖區内的資料全部寫完 |
- 直接強制關閉連接配接示例:
int close(int fd); |
1.9 檢視Linux系統目前的網絡連接配接
在/proc/net/tcp目錄下面儲存了目前系統所有TCP連結的狀态資訊。
檢視示例:
[root@wbyq FileSend2]# cat /proc/net/tcp sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode 0: 00000000:006F 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 13264 1 c16ac5c0 99 0 0 10 -1 1: 00000000:DA10 00000000:0000 0A 00000000:00000000 00:00000000 00000000 29 0 13592 1 c16ac0c0 99 0 0 10 -1 2: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 14400 1 c16acac0 99 0 0 10 -1 3: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 13851 1 c142f080 99 0 0 10 -1 4: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 14753 1 c142fa80 99 0 0 10 -1 5: 813DA8C0:A019 49AAC3CB:0522 01 00000000:00000000 00:00000000 00000000 0 0 123641 1 c142f580 20 3 18 10 -1 |
說明: 這裡的IP位址資訊和端口号都是使用十六進制儲存的。
813DA8C0:A019 49AAC3CB:0522表示:
- 檢視網絡狀态連接配接:
[root@wbyq FileSend2]# netstat -ntp Active Internet connections (w/o servers) Proto Recv-Q Send-Q Local AddressForeign Address State PID/Program name tcp 0 0 192.168.61.129:40985203.195.170.73:1314 ESTABLISHED 20955/./app_c |
從上面可得到的資訊:
連接配接類型: TCP協定
本地IP位址和端口号: 192.168.61.129:40985
與其通信的遠端IP位址和端口号: 203.195.170.73:1314
狀态: ESTABLISHED(已建立的連接配接)
程序PID号與應用程式名稱: 20955/./app_c
- socket網絡連接配接的狀态如下
1、LISTENING狀态
FTP服務啟動後首先處于偵聽(LISTENING)狀态。
2、ESTABLISHED狀态
ESTABLISHED的意思是建立連接配接。表示兩台機器正在通信。
3、CLOSE_WAIT
對方主動關閉連接配接或者網絡異常導緻連接配接中斷,這時我方的狀态會變成CLOSE_WAIT 此時我方要調用close()來使得連接配接正确關閉
4、TIME_WAIT
我方主動調用close()斷開連接配接,收到對方确認後狀态變為TIME_WAIT。TCP協定規定TIME_WAIT狀态會一直持續2MSL(即兩倍的分 段最大生存期),以此來確定舊的連接配接狀态不會對新連接配接産生影響。處于TIME_WAIT狀态的連接配接占用的資源不會被核心釋放,是以作為伺服器,在可能的情 況下,盡量不要主動斷開連接配接,以減少TIME_WAIT狀态造成的資源浪費。
目前有一種避免TIME_WAIT資源浪費的方法,就是關閉socket的LINGER選項。但這種做法是TCP協定不推薦使用的,在某些情況下這個操作可能會帶來錯誤。
5、SYN_SENT狀态
SYN_SENT狀态表示請求連接配接,當你要通路其它的計算機的服務時首先要發個同步信号給該端口,此時狀态為SYN_SENT,如果連接配接成功了就變為 ESTABLISHED,此時SYN_SENT狀态非常短暫。但如果發現SYN_SENT非常多且在向不同的機器發出,那你的機器可能中了沖擊波或震蕩波 之類的了。這類為了感染别的計算機,它就要掃描别的計算機,在掃描的過程中對每個要掃描的計算機都要發出了同步請求,這也是出現許多 SYN_SENT的原因。
根據TCP協定定義的3次握手斷開連接配接規定,發起socket主動關閉的一方 socket将進入TIME_WAIT狀态,TIME_WAIT狀态将持續2個MSL(Max Segment Lifetime),在Windows下預設為4分鐘,即240秒,TIME_WAIT狀态下的socket不能被回收使用. 具展現象是對于一個處理大量短連接配接的伺服器,如果是由伺服器主動關閉用戶端的連接配接,将導緻伺服器端存在大量的處于TIME_WAIT狀态的socket, 甚至比處于Established狀态下的socket多的多,嚴重影響伺服器的處理能力,甚至耗盡可用的socket,停止服務. TIME_WAIT是TCP協定用以保證被重新配置設定的socket不會受到之前殘留的延遲重發封包影響的機制,是必要的邏輯保證.
第二章 UDP網絡程式設計
2.1 UDP協定建立流程
資料報收發函數
2.2.1 recvfrom函數
UDP使用recvfrom()函數接收資料,他類似于标準的read(),但是在recvfrom()函數中要指明資料的目的位址。
#include <sys/types.h> #include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * from, size_t *addrlen); |
傳回值
成功傳回接收到資料的長度,負數失敗
前三個參數等同于函數read()的前三個參數,flags參數是傳輸控制标志。最後兩個參數類似于accept的最後兩個參數(接收用戶端的IP位址)。
示例:
/*阻塞方式接收資料*/ int len=0; char buff[1024]; size_t addrlen=sizeof(struct sockaddr); while(1) { len=recvfrom(socketfd,buff,1024,0,(struct sockaddr *)&ClientSocket,&addrlen); buff[len]='\0'; printf("Rx: %s,len=%d\n",buff,len); printf("資料發送方IP位址:%s\n",inet_ntoa(ClientSocket.sin_addr)); printf("資料發送方端口号:%d\n",ntohs(ClientSocket.sin_port)); } |
2.2.2 sendto函數
UDP使用sendto()函數發送資料,他類似于标準的write(),但是在sendto()函數中要指明目的位址。
#include <sys/types.h> #include <sys/socket.h> ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr * to, int addrlen); |
傳回值
成功傳回發送資料的長度,失敗傳回-1
前三個參數等同于函數read()的前三個參數,flags參數是傳輸控制标志。參數to指明資料将發往的協定位址,他的大小由addrlen參數來指定。
示例:
/*向UDP協定伺服器發送資料*/ ServerSocket.sin_family=PF_INET; //協定 ServerSocket.sin_port=htons(PROT); //端口 ServerSocket.sin_addr.s_addr=inet_addr(argv[1]); //表示伺服器的IP位址 bzero(ServerSocket.sin_zero,8); //初始化空間 char buff[]="1234567890"; int len=0; while(1) { len=sendto(socketfd,buff,strlen(buff),0,(const struct sockaddr*)&ServerSocket,sizeof(struct sockaddr)); printf("Tx: %d\n",len); sleep(1); } |
第三章設定Socket套接字屬性
3.1 函數原型介紹
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen); int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen); |
參數
sockfd:辨別一個套接口的描述字。
level:選項定義的層次;目前僅支援SOL_SOCKET和IPPROTO_TCP層次。
optname:需設定的選項。
optval:指針,指向存放選項值的緩沖區。
optlen:optval緩沖區的長度。
3.2 屬性功能注釋
setsockopt()函數用于任意類型、任意狀态套接口的設定選項值。盡管在不同協定層上存在選項,但本函數僅定義了最高的“套接口”層次上的選項。選項影響套接口的操作,諸如加急資料是否在普通資料流中接收,廣播資料是否可以從套接口發送等等。
- setsockopt()支援的選項定義位置:/usr/include/asm-generic/socket.h
#ifndef __ASM_GENERIC_SOCKET_H #define __ASM_GENERIC_SOCKET_H #include <asm/sockios.h> /* For setsockopt(2) */ #define SOL_SOCKET1 #define SO_DEBUG1 #define SO_REUSEADDR2 #define SO_TYPE3 #define SO_ERROR4 #define SO_DONTROUTE5 #define SO_BROADCAST6 #define SO_SNDBUF7 #define SO_RCVBUF8 #define SO_SNDBUFFORCE32 #define SO_RCVBUFFORCE33 #define SO_KEEPALIVE9 #define SO_OOBINLINE10 #define SO_NO_CHECK11 #define SO_PRIORITY12 #define SO_LINGER13 #define SO_BSDCOMPAT14 /* To add :#define SO_REUSEPORT 15 */ #ifndef SO_PASSCRED /* powerpc only differs in these */ #define SO_PASSCRED16 #define SO_PEERCRED17 #define SO_RCVLOWAT18 #define SO_SNDLOWAT19 #define SO_RCVTIMEO20 #define SO_SNDTIMEO21 #endif /* Security levels - as per NRL IPv6 - don't actually do anything */ #define SO_SECURITY_AUTHENTICATION22 #define SO_SECURITY_ENCRYPTION_TRANSPORT23 #define SO_SECURITY_ENCRYPTION_NETWORK24 #define SO_BINDTODEVICE25 /* Socket filtering */ #define SO_ATTACH_FILTER26 #define SO_DETACH_FILTER27 #define SO_PEERNAME28 #define SO_TIMESTAMP29 #define SCM_TIMESTAMPSO_TIMESTAMP #define SO_ACCEPTCONN30 #define SO_PEERSEC31 #define SO_PASSSEC34 #define SO_TIMESTAMPNS35 #define SCM_TIMESTAMPNSSO_TIMESTAMPNS #define SO_MARK36 #define SO_TIMESTAMPING37 #define SCM_TIMESTAMPINGSO_TIMESTAMPING #define SO_PROTOCOL38 #define SO_DOMAIN39 #define SO_RXQ_OVFL40 #endif /* __ASM_GENERIC_SOCKET_H */ |
setsockopt()支援下列選項。其中“類型”表明optval所指資料的類型。
選項 | 類型 | 意義 |
SO_BROADCAST | BOOL | 允許套接口傳送廣播資訊。 |
SO_DEBUG | BOOL | 記錄調試資訊。 |
SO_DONTLINER | BOOL | 不要因為資料未發送就阻塞關閉操作。設定本選項相當于将SO_LINGER的l_onoff元素置為零。 |
SO_DONTROUTE | BOOL | 禁止選徑;直接傳送。 |
SO_KEEPALIVE | BOOL | 發送“保持活動”包。 |
SO_LINGER | struct linger FAR* | 如關閉時有未發送資料,則逗留。 |
SO_OOBINLINE | BOOL | 在正常資料流中接收帶外資料。 |
SO_RCVBUF | int | 為接收确定緩沖區大小。 |
SO_REUSEADDR | BOOL | 允許套接口和一個已在使用中的位址捆綁(參見bind())。 |
SO_SNDBUF | int | 指定發送緩沖區大小。 |
TCP_NODELAY BOOL | 禁止發送合并的Nagle算法。 |
3.3 設定socket具有廣播特性
發送UDP資料報的時候,設定socket具有廣播特性:(預設情況下socket不支援廣播特性)
const int opt = 1; //設定該套接字為廣播類型, int nb = 0; nb = setsockopt(client_fd, SOL_SOCKET, SO_BROADCAST, (char *)&opt, sizeof(opt)); if(nb == -1) { printf("設定廣播類型錯誤.\n"); } |
3.4 設定socket發送和接收的緩沖區大小。
系統預設的狀态發送和接收一次為8688位元組(約為8.5K);在實際的過程中發送資料和接收資料量比較大,可以設定socket緩沖區。
// 接收緩沖區 int nRecvBuf=20*1024;//設定為20K setsockopt(socketfd,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int)); //發送緩沖區 int nSendBuf=20*1024;//設定為20K setsockopt(socketfd,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int)); |
3.5 設定收發時限
在發送和接收過程中有時由于網絡狀況等原因,發收不能預期進行,而設定收發時限:
int nNetTimeout=1000;//1秒 //發送時限 setsockopt(socketfd,SOL_SOCKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int)); //接收時限 setsockopt(socketfd,SOL_SOCKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int)); |
3.6 允許套接字綁定已使用的端口
有時候将伺服器關閉之後,端口的釋放需要時間,可以設定該資料允許套接字綁定正在被占用的端口。
int on = 1; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); |
3.7 忽略SIGPIPE信号
往一個已經接收到FIN的套接中寫是允許的,接收到的FIN僅僅代表對方不再發送資料。并不能代表我不能發送資料給對方。
往一個FIN結束的程序中寫(write),對方會發送一個RST字段過來,TCP重置。
如果再調用write就會産生SIGPIPE信号。
(也就是當伺服器向用戶端發送資料時,用戶端突然斷開連接配接,會導緻SIGPIPE信号産生,如果不處理,系統預設的處理方式就終止程序)
signal(SIGPIPE,SIG_IGN); //忽略SIGPIPE信号 |
3.8 擷取網絡底層緩沖區發送剩餘位元組數
在網絡程式設計時,發送方調用write(fd)将封包發送的時候實際上隻是寫入了核心的write buffer。接收方什麼時候能收到封包是個未知數。
在某些需要同步狀态機的地方,發送方最好能夠确認接收方收到封包後再進行下一步動作。
linux提供了ioctl(fd, SIOCOUTQ, &count)方法來查詢一個tcp socket的write buffer是否清空。發送方一般可以用這個方法來判斷對端是否收到封包。當底層網卡将緩沖區的資料全部發送成功時,擷取的count=0
#include <sys/ioctl.h>
#include <linux/sockios.h>
int value;
ioctl(client_fd,SIOCOUTQ,&value);
3.9 擷取目前網絡協定底層發送與接收緩沖區大小
int sockfd;
/*1. 建立socket套接字*/
sockfd=socket(AF_INET,SOCK_STREAM,0);
int nRecvBuf;
socklen_t len=4;
getsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&nRecvBuf,&len);
printf("接收緩沖區大小=%d\n",nRecvBuf);
//發送緩沖區
int nSendBuf;
getsockopt(sockfd,SOL_SOCKET,SO_SNDBUF,&nSendBuf,&len);
printf("發送緩沖區大小=%d\n",nSendBuf);
Redhat6.3系統上輸出結果:
接收緩沖區大小=87380
發送緩沖區大小=16384
第四章 Linux核心下TCP/IP參數優化
4.1 檢視系統TCP/IP參數的預設值
所有的TCP/IP參數都位于/proc/sys/net目錄下。
(請注意,對/proc/sys/net目錄下内容的修改都是臨時的,任何修改在系統重新開機後都會丢失)
例如下面這些重要的參數:
參數(路徑+檔案) | 描述 | 預設值 | 優化值 |
/proc/sys/net/core/rmem_default | 預設的TCP資料接收視窗大小(位元組)。 | 229376 | 256960 |
/proc/sys/net/core/rmem_max | 最大的TCP資料接收視窗(位元組)。 | 131071 | 513920 |
/proc/sys/net/core/wmem_default | 預設的TCP資料發送視窗大小(位元組)。 | 229376 | 256960 |
/proc/sys/net/core/wmem_max | 最大的TCP資料發送視窗(位元組)。 | 131071 | 513920 |
/proc/sys/net/core/optmem_max | 表示每個套接字所允許的最大緩沖區的大小。 | 20480 | 81920 |
4.1.2 檢視TCP接收緩沖區的預設值
[root@wbyq /]# cat /proc/sys/net/ipv4/tcp_rmem 4096873803493888 其中87380表示tcp接收緩沖區的預設值 |
4.1.3 檢視TCP發送緩沖區的預設值
[root@wbyq /]# cat /proc/sys/net/ipv4/tcp_wmem
|
4.1.4 tcp或udp接收緩沖區最大值
[root@wbyq /]# cat /proc/sys/net/core/rmem_max 131071 |
其中131071表示tcp 或 udp 接收緩沖區最大可設定值的一半。
也就是說調用 setsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen); 函數時rcv_size大小如果超過 131071,那麼getsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen); 擷取的值就等于 131071 * 2 = 262142
4.1.5 tcp或udp發送緩沖區最大值
[root@wbyq /]# cat /proc/sys/net/core/wmem_max 131071 |
其中131071表示tcp 或 udp 發送緩沖區最大可設定值的一半。
設定方法與上面接收緩沖區同理。
4.1.6 udp收發緩沖區預設值
[root@wbyq /]# cat /proc/sys/net/core/rmem_default //接收緩沖區預設值 188416 [root@wbyq /]# cat /proc/sys/net/core/wmem_default //發送緩沖區預設值 188416 |
4.1.7 tcp 或udp收發緩沖區最小值
tcp 或udp接收緩沖區的最小值為 256 bytes,由核心的宏決定
tcp 或udp發送緩沖區的最小值為 2048 bytes,由核心的宏決定
4.1.8 TCP重傳次數
[root@wbyq /]# cat /proc/sys/net/ipv4/tcp_retries2 15 |
4.2 指令行直接修改系統TCP/IP參數值
- 修改系統套接字緩沖區:
echo 65536 > /proc/sys/net/core/rmem_max
echo 256960 > /proc/sys/net/core/wmem_max
echo 65536 > /proc/sys/net/core/wmen_default
- 修改tcp接收/發送緩沖區:
echo "4096 32768 65536" > /proc/sys/net/ipv4/tcp_rmem
echo "4096 65536 256960" > /proc/sys/net/ipv4/tcp_wmem
- 重傳次數:
echo 5 > /proc/sys/net/ipv4/tcp_retries2
4.3 通過代碼修改TCP/IP緩沖區大小
系統提供的socket緩沖區大小為8K,你可以将之設定為64K,尤其在傳輸大檔案或者視訊檔案的時候。
socket發送資料時候先把資料發送到socket緩沖區中,之後接受函數再從緩沖區中取資料,如果發送端特别快的時候,緩沖區很快就被填滿(socket預設的是1024×8=8192位元組),這時候我們應該根據情況設定緩沖區的大小,可以通過setsockopt函數實作。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <assert.h> int main(int argc,char **argv) { int err = -1; /* 傳回值 */ int s = -1; /* socket描述符 */ int snd_size = 0; /* 發送緩沖區大小 */ int rcv_size = 0; /* 接收緩沖區大小 */ socklen_t optlen; /* 選項值長度 */ /* * 建立一個TCP套接字 */ s = socket(PF_INET,SOCK_STREAM,0); if( s == -1){ printf("建立套接字錯誤\n"); return -1; } /* * 先讀取緩沖區設定的情況 * 獲得原始發送緩沖區大小 */ optlen = sizeof(snd_size); err = getsockopt(s, SOL_SOCKET, SO_SNDBUF,&snd_size, &optlen); if(err<0) { printf("擷取發送緩沖區大小錯誤\n"); } /* * 列印原始緩沖區設定情況 */ /* * 獲得原始接收緩沖區大小 */ optlen = sizeof(rcv_size); err = getsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen); if(err<0) { printf("擷取接收緩沖區大小錯誤\n"); } printf(" 發送緩沖區原始大小為: %d 位元組\n",snd_size); printf(" 接收緩沖區原始大小為: %d 位元組\n",rcv_size); /* * 設定發送緩沖區大小 */ snd_size = 10*1024; /* 發送緩沖區大小為8K */ optlen = sizeof(snd_size); err = setsockopt(s, SOL_SOCKET, SO_SNDBUF, &snd_size, optlen); if(err<0) { printf("設定發送緩沖區大小錯誤\n"); } /* * 設定接收緩沖區大小 */ rcv_size = 10*1024; /* 接收緩沖區大小為8K */ optlen = sizeof(rcv_size); err = setsockopt(s,SOL_SOCKET,SO_RCVBUF, (char *)&rcv_size, optlen); if(err<0) { printf("設定接收緩沖區大小錯誤\n"); } /* * 檢查上述緩沖區設定的情況 * 獲得修改後發送緩沖區大小 */ optlen = sizeof(snd_size); err = getsockopt(s, SOL_SOCKET, SO_SNDBUF,&snd_size, &optlen); if(err<0) { printf("擷取發送緩沖區大小錯誤\n"); } /* * 獲得修改後接收緩沖區大小 */ optlen = sizeof(rcv_size); err = getsockopt(s, SOL_SOCKET, SO_RCVBUF,(char *)&rcv_size, &optlen); if(err<0) { printf("擷取接收緩沖區大小錯誤\n"); } /* * 列印結果 */ printf(" 發送緩沖區大小為: %d 位元組\n",snd_size); printf(" 接收緩沖區大小為: %d 位元組\n",rcv_size); close(s); return 0; } |
運作結果分析:
[root@wbyq demo_code2]# ./a.out 發送緩沖區原始大小為: 16384 位元組 接收緩沖區原始大小為: 87380 位元組 發送緩沖區大小為: 20480 位元組 接收緩沖區大小為: 20480 位元組 |
設定的接收和發送緩沖區大小為:10*1024=10240位元組,實際用getoptsock得到是20480位元組,實際加了一倍。
4.4 Socket接收緩沖區分析
Socket接收緩沖區每次接收的資料會使用追加的方式存放在緩沖區裡。
比如: 用戶端給伺服器一次發送了10個位元組的資料,伺服器每次調用read函數讀取一個位元組,分10次讀取也可以将資料全部讀取到。
如果用戶端給伺服器一次發送了大量資料,伺服器來不及接收,隻要伺服器端的接收緩沖區不溢出,收到的資料都會追加方式存放在接收緩沖區裡。
示例代碼:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdlib.h> #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <string.h> unsigned char rx_buff; /* TCP伺服器建立 */ int main(int argc,char **argv) { int tcp_server_fd; //伺服器套接字描述符 int tcp_client_fd; //用戶端套接字描述符 struct sockaddr_in tcp_server; struct sockaddr_in tcp_client; socklen_t tcp_client_addrlen=0; int tcp_server_port; //伺服器的端口号 //判斷傳入的參數是否合理 if(argc!=2) { printf("參數格式:./tcp_server <端口号>\n"); return -1; } tcp_server_port=atoi(argv[1]); //将字元串轉為整數 /*1. 建立網絡套接字*/ tcp_server_fd=socket(AF_INET,SOCK_STREAM,0); if(tcp_server_fd<0) { printf("TCP伺服器端套接字建立失敗!\n"); return -1; } /*2. 綁定端口号,建立伺服器*/ tcp_server.sin_family=AF_INET; //IPV4協定類型 tcp_server.sin_port=htons(tcp_server_port);//端口号指派,将本地位元組序轉為網絡位元組序 tcp_server.sin_addr.s_addr=INADDR_ANY; //将本地IP位址指派給結構體成員 if(bind(tcp_server_fd,(const struct sockaddr*)&tcp_server,sizeof(struct sockaddr))<0) { printf("TCP伺服器端口綁定失敗!\n"); return -1; } /*3. 設定監聽的用戶端數量*/ listen(tcp_server_fd,10); /*4. 等待用戶端連接配接*/ tcp_client_addrlen=sizeof(struct sockaddr); tcp_client_fd=accept(tcp_server_fd,(struct sockaddr *)&tcp_client,&tcp_client_addrlen); if(tcp_client_fd<0) { printf("TCP伺服器:等待用戶端連接配接失敗!\n"); return -1; } //列印連接配接的用戶端位址資訊 printf("已經連接配接的用戶端資訊: %s:%d\n",inet_ntoa(tcp_client.sin_addr),ntohs(tcp_client.sin_port)); /*5. 資料通信*/ fd_set readfds; //讀事件的檔案操作集合 int select_state,rx_cnt; //接收傳回值 while(1) { /*5.1 清空檔案操作集合*/ FD_ZERO(&readfds); /*5.2 添加要監控的檔案描述符*/ FD_SET(tcp_client_fd,&readfds); /*5.3 監控檔案描述符*/ select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,NULL); if(select_state>0)//表示有事件産生 { /*5.4 測試指定的檔案描述符是否産生了讀事件*/ if(FD_ISSET(tcp_client_fd,&readfds)) { /*5.5 讀取資料 每次讀取一個位元組資料*/ rx_cnt=read(tcp_client_fd,&rx_buff,1); printf("%d,%c\n",rx_cnt,rx_buff); if(rx_cnt==0) { printf("對方已經斷開連接配接!\n"); break; } } } else if(select_state<0) //表示産生了錯誤 { printf("select函數産生異常!\n"); break; } } /*6. 關閉連接配接*/ |