在進行網絡編程的時候,經常需要査看或者設置套接字的某些特性,例如設置地址複用、讀寫數據的超時時間、對讀緩衝區的大小進行調整等操作。獲得套接字選項設置情況 的函數是getsockopt(),設置套接字選項的函數爲setsockopt()。
功能描述:
獲取或者設置與某個套接字關聯的選項。選項可能存在於多層協議中,它們總會出現在最上面的套接字層。當操作套接字選項時,選項位於的層和選項的名稱必須給出。爲了操作套接字層的選項,應該將層的值指定爲SOL_SOCKET。爲了操作其它層的選項,控制選項的合適協議號必須給出。例如,爲了表示一個選項由TCP協議解析,層應該設定爲協議號TCP。
用法:
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
參數:
- sock:將要被設置或者獲取選項的套接字。
- level:選項所在的協議層。
- optname:需要訪問的選項名。
- optval:對於getsockopt(),指向返回選項值的緩衝。對於setsockopt(),指向包含新選項值的緩衝。
- optlen:對於getsockopt(),作爲入口參數時,選項值的最大長度。作爲出口參數時,選項值的實際長度。對於setsockopt(),現選項的長度。
返回說明:
成功執行時,返回0。失敗返回-1,errno被設爲以下的某個值
- EBADF:sock不是有效的文件描述詞
- EFAULT:optval指向的內存並非有效的進程空間
- EINVAL:在調用setsockopt()時,optlen無效
- ENOPROTOOPT:指定的協議層不能識別選項
- ENOTSOCK:sock描述的不是套接字
level參數:
level指定控制套接字的層次.可以取三種值:
- 1)SOL_SOCKET:通用套接字選項.
- 2)IPPROTO_IP:IP選項.
- 3)IPPROTO_TCP:TCP選項.
選項名稱 說明 數據類型
========================================================================
SOL_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
========================================================================
IPPROTO_IP
------------------------------------------------------------------------
IP_HDRINCL 在數據包中包含IP首部 int
IP_OPTINOS IP首部選項 int
IP_TOS 服務類型
IP_TTL 生存時間 int
========================================================================
IPPRO_TCP
------------------------------------------------------------------------
TCP_MAXSEG TCP最大數據段的大小 int
TCP_NODELAY 不使用Nagle算法 int
========================================================================
(1)SO_TYPE
這個選項用於設置或者獲得套接字的類型,例如SOCK_STREAM或者SOCK_DGRAM等表不套接字類型的數值。這個套接字選項經常用在忘記自己套接字類型或者不知道套接字類型的情況。
/************************************************************
*Copyright (C),lcb0281at163.com lcb0281atgmail.com
*FileName: socketopt_type.c
*BlogAddr: caibiao-lee.blog.csdn.net
*Description:
*Date: 2020-01-04
*Author: Caibiao Lee
*Version: V1.0
*Others:
*History:
***********************************************************/
#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 so_type = -1; /* Socket 類型 */
socklen_t len = -1; /* 選項值長度 */
/*
* 建立一個流式套接字
*/
s = socket(AF_INET,SOCK_STREAM,0);
if(-1 == s){
printf("socket error\n");
return -1;
}
/*
* 獲得SO_TYPE的值
*/
len = sizeof(so_type);
err = getsockopt(s, SOL_SOCKET, SO_TYPE, &so_type,&len);
if(err == -1){
printf("getsockopt error\n");
close(s);
return -1;
}
/*
* 輸出結果
*/
printf("socket fd: %d\n",s);
printf(" SO_TYPE : %d\n",so_type);
printf(" SO_STREAM = %d\n",SOCK_STREAM);
close(s);
return 0;
}
(2)SO_REUSERADDR
這個參數表示允許重複使用本地地址和端口,這個設置在服務器程序中經常使用。
例如某個服務器進程佔用了TCP的80端口進行偵聽,當再次在此端U偵聽時會返回錯誤。設置SO_REUSEADDR可以解決這個問題,允許共用這個端口。某些非正常退出服務器程序,可能需要佔用端段時間才能允許其他進程使用,即使這個程序已經死掉內核仍然要一段時間才能釋放此端口,不設置SO_REUSEADDR將不能正確綁定端口。
optval = 1; /* 重用有效 */
optlen = sizeof(optval);
err=setsockopt(s, SOL_SOCKET, SO_REUSEADDR,(char *)&optval, optlen);
if(err!= -1)
{ /* 設置失敗 */
printf("套接字可重用設置失敗!\n");
return -1;
}
(3)SO_KEEPALIVE
選項SO_KEEPALIVE用於設置TCP連接的保持,當設置此項後,連接會測試連接的狀態。這個選項用於可能長時間沒有數據交流的連接,通常在服務器端進行設置。
當設置SO_KEEPALIVE選項後,如果在兩個小時內沒有數據通信時,TCP會自動發送一個活動探測數據報文,對方必須對此進行響應,通常有如下3種情況。
- TCP的連接正常,發送一個ACK響應,這個過程應用層是不知道的。再過兩個小時,又會再發送一個。
- 對方發送RST響應,對方在2個小時內進行了重啓或者崩潰。之前的連接己經失效,套接字收到一個ECONNRESET錯誤,之前的套接字關閉。
- 如果對方沒有任何響應,則本機會發送另外8個活動探測報文,時間的間隔爲75s,當第一個活動報文發送11分15秒後仍然沒有收到對方的任何響應,則放棄探測,套接字錯誤類型設置爲ETIMEOUT,並關閉套接字連接。如果收到一個ICMP控制報文響應,此時套接字也關閉,這種情況通常收到的是一個主機不可達的ICMP報文,此時套接字錯誤類型設置爲EHOSTUNREACH,並關閉套接字連接。
SO_KEEPALIVE的使用場景主要是在可能發送長時間無數據響應的TCP連接,例如Telnet會話,經常會出現打開一個telnet客戶端後,長時間不用的情況,這需要服務器或 者客戶端有一個探測機制知道對方是否仍然活動。根據探測結果服務器會釋放己經失效的客戶端,保證服務器資源的有效性,例如有的telnet客戶端沒有按照正常步驟進行關閉。
socketopt_keepalive_server.c
/************************************************************
*Copyright (C),lcb0281at163.com lcb0281atgmail.com
*FileName: socketopt_keepalive_server.c
*BlogAddr: caibiao-lee.blog.csdn.net
*Description:
*Date: 2020-01-04
*Author: Caibiao Lee
*Version: V1.0
*Others:
*History:
***********************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<error.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<unistd.h>
#define PORT 8888 /* 服務器偵聽端口爲8888 */
#define BACKLOG 8 /* 最大偵聽排隊數量爲8 */
static int alive = 1; /* 是否退出 */
/* 用於處理SIGPIP和SIGINT信號的函數 */
static int sigpipe(int signo)
{
alive = 0;
}
int main(int argc, char *argv[])
{
/* s爲服務器的偵聽套接字描述符,sc爲客戶端連接成功返回的描述符 */
int s, sc;
/* local_addr本地地址,client_addr客戶端的地址 */
struct sockaddr_in local_addr,client_addr;
int err = -1; /* 錯誤返回值 */
socket_t optlen = -1; /* 整型的選項類型值 */
int optval = -1; /* 選項類型值長度 */
/* 截取SIGPIPE和SIGINT由函數signo處理 */
signal(SIGPIPE, signo);
signal(SIGINT,signo);
/* 創建本地監聽套接字 */
s = socket(AF_INET,SOCK_STREAM,0);
if( s == -1)
{
printf("套接字創建失敗!\n");
return -1;
}
/* 設置地址和端口重用 */
optval = 1; /* 重用有效 */
optlen = sizeof(optval);
err=setsockopt(s, SOL_SOCKET, SO_REUSEADDR,(char *)&optval, optlen);
if(err!= -1)
{ /* 設置失敗 */
printf("套接字可重用設置失敗!\n");
return -1;
}
/* 初始化本地協議族,端口和IP地址 */
bzero(&local_addr, 0, sizeof(local_addr)); /* 清理 */
local_addr.sin_family=AF_INET; /* 協議族 */
local_addr.sin_port=htons(PORT); /* 端口 */
local_addr.sin_addr.s_addr=INADDR_ANY; /* 任意本地地址 */
/* 綁定套接字 */
err = bind(s, (struct sockaddr *)&local_addr, sizeof(struct sockaddr);
if(err == -1)
{ /* 綁定失敗 */
printf("綁定失敗!\n");
return -1;
}
/* 設置最大接收緩衝區和最大發送緩衝區 */
optval = 128*1024; /* 緩衝區大小爲128K */
optlen = sizeof(optval);
err = setsockopt(s, SOL_SOCKET, SO_RCVBUF, &optval, optlen);
if(err == -1)
{/* 設置接收緩衝區大小失敗 */
printf("設置接收緩衝區失敗\n");
}
err = setsockopt(s, SOL_SOCKET, SO_SNDBUF, &optval, optlen);
if(err == -1)
{/* 設置發送緩衝區大小失敗 */
printf("設置發送緩衝區失敗\n");
}
/* 設置發送和接收超時時間 */
struct timeval tv;
tv.tv_sec = 1; /* 1秒 */
tv.tv_usec = 200000;/* 200ms */
optlen = sizeof(tv);
err = setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, optlen); /* 設置接收超時時間 */
if(err == -1)
{/* 設置接收超時時間失敗 */
printf("設置接收超時時間失敗\n");
}
err = setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, optlen);/* 設置發送超時時間 */
if(err == -1){
printf("設置發送超時時間失敗\n");
}
/* 設置監聽 */
err = listen(s,BACKLOG);
if( err ==-1)
{ /* 設置監聽失敗 */
printf("設置監聽失敗!\n");
return -1;
}
printf("等待連接...\n");
fd_set fd_r; /* 讀文件描述符集 */
struct timeval tv;
tv.tv_usec = 200000; /* 超時時間爲200ms */
tv.tv_sec = 0;
while(alive)
{
//有連接請求時進行連接
socklen_t sin_size=sizeof(struct sockaddr_in);
/* 此處每次會輪詢是否有客戶端連接到來,間隔時間爲200ms */
FD_ZERO(&fd_r); /* 清除文件描述符集 */
FD_SET(s, &fd_r); /* 將偵聽描述符放入 */
switch (select(s + 1, &fd_r, NULL, &tv))
{ /* 監視文件描述符集fd_r */
case -1: /* 錯誤發生 */
case 0: /* 超時 */
continue;
break;
default: /* 有連接到來 */
break;
}
/* 有連接到來,接收... */
sc = accept(s, (struct sockaddr *)&client_addr,&sin_size);
if( sc ==-1)
{ /* 失敗 */
perror("接受連接失敗!\n");
continue;
}
/* 設置連接探測超時時間 */
optval = 10; /* 10秒 */
optlen = sizeof(optval);/**/
err = setsockopt(sc, IPPROTO_TCP, SO_KEEPALIVE, (char*)&optval, optlen);/* 設置... */
if( err == -1)
{/* 失敗 */
printf("設置連接探測間隔時間失敗\n");
}
/* 設置禁止Nagle算法 */
optval = 1; /* 禁止 */
optlen = sizeof(optval);
err = setsockopt(sc, IPPROTO_TCP, TCP_NODELAY, (char*)&optval, optlen);/* 設置... */
if( err == -1)
{/* 失敗 */
printf("禁止Nagle算法失敗\n");
}
/* 設置連接延遲關閉爲立即關閉 */
struct linger;
linger.l_onoff = 1; /* 延遲關閉生效 */
linger.l_linger = 0; /* 立即關閉 */
optlen = sizeof(linger);
err = setsockopt(sc, SOL_SOCKET, SO_LINGER, (char*)&linger, optlen);/* 設置... */
if( err == -1)
{/* 失敗 */
printf("設置立即關閉失敗\n");
}
/* 打印客戶端IP地址信息 */
printf("接到一個來自%s的連接\n",inet_ntoa(client_addr.sin_addr));
err = send(sc,"連接成功!\n",10,0);
if(err == -1)
{
printf("發送通知信息失敗!\n");
}
/* 關閉客戶端連接 */
close(sc);
}
/* 關閉服務器端 */
close(s);
return 0;
}
(4)SO_RCVBUF/SO_SNDBUF
選項SO_RCVBUF和SO_SNDBUF用於操作發送緩衝區和接收緩衝區的大小,對於每個套接字對應均有發送緩衝區和接收緩衝區。接收緩衝區用於保存網絡協議棧收到的數據,直到應用程序成功地讀取:發送緩衝區則需要保存發送的數據直到發送成功。
這兩個選項在TCP連接和UDP連接中的含義有所不同
在UDP連接中,由於它是無狀態連接,發送緩衝區在數據通過網絡設備發送後就可以丟棄,不用保存。而接收緩衝區則需要保存數據直到應用程序讀取,由於UDP沒有流量 控制,當緩衝區過小時,發送端局部時間內會產生爆發性數據傳輸,由於接收端來不及讀取數據,很容易造成緩衝區溢出,將原來的數據覆蓋,淹沒接收端。因此使用UDP連接時, 需要將接收的緩衝區調整爲比較大的值。
在TCP連接中,接收緩衝區大小就是滑動窗口大小。TCP的接收緩衝區不可能溢出,因爲不允許對方發送超過接收緩衝區大小的數據,當對方發送的數據超過滑動窗口大小,接收方會將數據丟棄。
設置TCP接收緩衝區大小的時機很重要,因爲接收緩衝區與滑動窗口的大小是一致的,而滑動窗口的協商是在建立連接時通過SYN獲得的。對於客戶端程序,接收緩衝區的大小要在connect()函數調用之前進行設置,因爲connect()需要通過SYN建立連接。而對於服務器程序,需要在listen()之前進行設置接收緩衝區的大小,因爲accept()返回的套接字描述符是繼承了listen()的描述符屬性,此時的滑動窗口都己經進行了設置。
/************************************************************
*Copyright (C),lcb0281at163.com lcb0281atgmail.com
*FileName: socketopt_bufsize.c
*BlogAddr: caibiao-lee.blog.csdn.net
*Description:
*Date: 2020-01-04
*Author: Caibiao Lee
*Version: V1.0
*Others:
*History:
***********************************************************/
#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){
printf("獲取發送緩衝區大小錯誤\n");
}
/*
* 打印原始緩衝區設置情況
*/
printf(" 發送緩衝區原始大小爲: %d 字節\n",snd_size);
printf(" 接收緩衝區原始大小爲: %d 字節\n",rcv_size);
/*
* 獲得原始接收緩衝區大小
*/
optlen = sizeof(rcv_size);
err = getsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen);
if(err){
printf("獲取接收緩衝區大小錯誤\n");
}
/*
* 設置發送緩衝區大小
*/
snd_size = 4096; /* 發送緩衝區大小爲8K */
optlen = sizeof(snd_size);
err = setsockopt(s, SOL_SOCKET, SO_SNDBUF, &snd_size, optlen);
if(err){
printf("設置發送緩衝區大小錯誤\n");
}
/*
* 設置接收緩衝區大小
*/
rcv_size = 8192; /* 接收緩衝區大小爲8K */
optlen = sizeof(rcv_size);
err = setsockopt(s,SOL_SOCKET,SO_RCVBUF, &rcv_size, optlen);
if(err){
printf("設置接收緩衝區大小錯誤\n");
}
/*
* 檢查上述緩衝區設置的情況
* 獲得修改後發送緩衝區大小
*/
optlen = sizeof(snd_size);
err = getsockopt(s, SOL_SOCKET, SO_SNDBUF,&snd_size, &optlen);
if(err){
printf("獲取發送緩衝區大小錯誤\n");
}
/*
* 獲得修改後接收緩衝區大小
*/
optlen = sizeof(rcv_size);
err = getsockopt(s, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen);
if(err){
printf("獲取接收緩衝區大小錯誤\n");
}
/*
* 打印結果
*/
printf(" 發送緩衝區大小爲: %d 字節\n",snd_size);
printf(" 接收緩衝區大小爲: %d 字節\n",rcv_size);
close(s);
return 0;
}
(5)OPTION_SHOW
顯示設備中的所有選項默認參數值,可以使用下面的代碼:
/************************************************************
*Copyright (C),lcb0281at163.com lcb0281atgmail.com
*FileName: socket_show.c
*BlogAddr: caibiao-lee.blog.csdn.net
*Description:
*Date: 2020-01-04
*Author: Caibiao Lee
*Version: V1.0
*Others:
*History:
***********************************************************/
#include <netinet/tcp.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <linux/in.h>
#include <unistd.h>
/* 結構保存獲取結果 */
typedef union optval
{
int val; /*整型值*/
struct linger linger; /*linger結構*/
struct timeval tv; /*時間結構*/
unsigned char str[16]; /*字符串*/
}val;
/*數值類型*/
typedef enum valtype
{
VALINT, /*int類型*/
VALLINGER, /*struct linger類型*/
VALTIMEVAL, /*struct timeval類型*/
VALUCHAR, /*字符串*/
VALMAX /*錯誤類型*/
}valtype;
/* 用於保存套接字選項的結構 */
typedef struct sopts
{
int level; /*套接字選項級別*/
int optname; /*套接字選項名稱*/
char *name; /*套接字名稱*/
valtype valtype;/*套接字返回參數類型*/
}sopts;
static val optval;/*用於保存數值*/
sopts sockopts[] =
{
{SOL_SOCKET, SO_BROADCAST, "SO_BROADCAST", VALINT},
{SOL_SOCKET, SO_DEBUG, "SO_DEBUG", VALINT},
{SOL_SOCKET, SO_DONTROUTE, "SO_DONTROUTE", VALINT},
{SOL_SOCKET, SO_ERROR, "SO_ERROR", VALINT},
{SOL_SOCKET, SO_KEEPALIVE, "SO_KEEPALIVE", VALINT},
{SOL_SOCKET, SO_LINGER, "SO_LINGER", VALINT},
{SOL_SOCKET, SO_OOBINLINE, "SO_OOBINLINE", VALINT},
{SOL_SOCKET, SO_RCVBUF, "SO_RCVBUF", VALINT},
{SOL_SOCKET, SO_RCVLOWAT, "SO_RCVLOWAT", VALINT},
{SOL_SOCKET, SO_RCVTIMEO, "SO_RCVTIMEO", VALTIMEVAL},
{SOL_SOCKET, SO_SNDTIMEO, "SO_SNDTIMEO", VALTIMEVAL},
{SOL_SOCKET, SO_TYPE, "SO_TYPE", VALINT},
{IPPROTO_IP, IP_HDRINCL, "IP_HDRINCL", VALINT},
{IPPROTO_IP, IP_OPTIONS, "IP_OPTIONS", VALINT},
{IPPROTO_IP, IP_TOS, "IP_TOS", VALINT},
{IPPROTO_IP, IP_TTL, "IP_TTL", VALINT},
{IPPROTO_IP, IP_MULTICAST_TTL, "IP_MULTICAST_TTL", VALUCHAR},
{IPPROTO_IP, IP_MULTICAST_LOOP, "IP_MULTICAST_LOOP", VALUCHAR},
{IPPROTO_TCP, TCP_KEEPCNT, "TCP_KEEPCNT", VALINT},
{IPPROTO_TCP, TCP_MAXSEG, "TCP_MAXSEG", VALINT},
{IPPROTO_TCP, TCP_NODELAY, "TCP_NODELAY", VALINT},
{0, 0, NULL, VALMAX}/*結尾,主程序中判斷VALMAX*/
};
/* 顯示查詢結果 */
static void disp_outcome(sopts *sockopt, int len, int err)
{
if(err == -1){/* 錯誤 */
printf("optname %s NOT support\n",sockopt->name);
return;
}
switch(sockopt->valtype){/*根據不同的類型進行信息打印*/
case VALINT:/*整型*/
printf("optname %s: default is %d\n",sockopt->name,optval.val);
break;
case VALLINGER:/*struct linger*/
printf("optname %s: default is %d(ON/OFF), %d to linger\n",
sockopt->name, /*名稱*/
optval.linger.l_onoff,/*linger打開*/
optval.linger.l_linger);/*延時時間*/
break;
case VALTIMEVAL:/*struct timeval結構*/
printf("optname %s: default is %.06f\n",
sockopt->name,/*名稱*/
((((double)optval.tv.tv_sec*100000+(double)optval.tv.tv_usec))/(double)1000000));/*浮點型結構*/
break;
case VALUCHAR:/*字符串類型,循環打印*/
{
int i = 0;
printf("optname %s: default is ",sockopt->name);/*選項名稱*/
for(i = 0; i < len; i++){
printf("%02x ", optval.str[i]);
}
printf("\n");
}
break;
default:
break;
}
}
int main(int argc, char *argv[])
{
int err = -1;
int len = 0;
int i = 0;
int s = socket(AF_INET, SOCK_STREAM, 0);/*建立一個流式套接字*/
while(sockopts[i].valtype != VALMAX)
{/*判斷是否結尾,否則輪詢執行*/
len = sizeof(sopts);/*計算結構長度*/
err = getsockopt(s, sockopts->level, sockopts->optname, &optval, &len);/*獲取選項狀態*/
disp_outcome(&sockopts[i], len, err);/*顯示結果*/
i++;/*遞增*/
}
close(s);
return 0;
}
文章內容來源於《linux網絡編程》