套接字的多種可選項
具體源碼可以參考我的GitHub
1. 套接字可選項和I/O緩衝大小
我們進行套接字編程時往往只關注數據通信,而忽略了套接字具有的不同特性。但是,理解這些特性並根據實際需要進行更改也很重要.
1.1 套接字的多種可選項
可選項是分層的。
協議層 | 選項名 | 讀取 | 設置 |
---|---|---|---|
SOL_SOCKET | SO_SNDBUF | O | O |
SOL_SOCKET | SO_RCVBUF | O | O |
SOL_SOCKET | SO_REUSEADDR | O | O |
SOL_SOCKET | SO_KEEPALIVE | O | O |
SOL_SOCKET | SO_BROADCAST | O | O |
SOL_SOCKET | SO_DONTROUTE | O | O |
SOL_SOCKET | SO_OOBINLINE | O | O |
SOL_SOCKET | SO_ERROR | O | X |
SOL_SOCKET | SO_TYPE | O | X |
IPPROTO_IP | IP_TOS | O | O |
IPPROTO_IP | IP_TTL | O | O |
IPPROTO_IP | IP_MULTICAST_TTL | O | O |
IPPROTO_IP | IP_MULTICAST_LOOP | O | O |
IPPROTO_IP | IP_MULTICAST_IF | O | O |
IPPROTO_TCP | TCP_KEEPALIVE | O | O |
IPPROTO_TCP | TCP_NODELAY | O | O |
IPPROTO_TCP | TCP_MAXSEG | O | O |
大致常用的協議層分三類
SOL_SOCKET層:是套接字的相關通用可選項
IPPROTO_IP層:是IP協議相關事項
IPPROTO_TCP層:是TCP協議相關事項
1.2 常用操作
有以下兩個操作Get讀取 and Set 設置。
讀取函數
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
成功返回0, 失敗返回-1
sock --> 套接字描述符
level --> 要查看的協議層
optname --> 可選項名
optval --> 保存查看結果的緩衝地址值
optlen --> 向第四個參數optval 傳遞的緩衝大小
設置函數
#include <sys/socket.h>
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
成功0 失敗 -1
optval 保存要修改的選項信息緩衝地址值
optlen 傳遞可選項信息的字節數
其他同上
調用getsockopt函數的事例如下:
- [sokc_type]
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main() {
int TCP_sock, UDP_sock;
int sock_type;
socklen_t optlen;
int state;
TCP_sock = socket(PF_INET, SOCK_STREAM, 0);
UDP_sock = socket(PF_INET, SOCK_DGRAM, 0);
printf("SOCK_STREAM: %d\n SOCK_DGRAM: %d\n", SOCK_STREAM, SOCK_DGRAM);
printf("PF_INET: %d\n", PF_INET);
state = getsockopt(TCP_sock, SOL_SOCKET, SO_TYPE, (void*) &sock_type, &optlen);
if (state)
error("getsockopt() error");
printf("TCP_sock type : %d \n optlen: %d\n", sock_type, optlen);
state = getsockopt(UDP_sock, SOL_SOCKET, SO_TYPE, (void*) &sock_type, &optlen);
if (state)
error("getsockopt() error");
printf("UDP_sock type : %d \n optlen: %d\n", sock_type, optlen);
close(TCP_sock);
close(UDP_sock);
return 0;
}
- 注意事項: 運行有時候會發生錯誤,多運行幾次即可。
- 套接字類型只能在創建決定,後期不能更改。
編譯運行:
gcc sock_type.c -o sock_type
./sock_type
運行結果:
SOCK_STREAM: 1
SOCK_DGRAM: 2
PF_INET: 2
TCP_sock type : 1
optlen: 4
UDP_sock type : 2
optlen: 4
1.3 I/O緩衝大小
生成套接字的時候將同時生成I/O緩衝。
SO_RCVBUF是輸入緩衝大小可選項,SO_SNDBUF是輸出緩衝大小可選項
讀取與更改事例
讀取
- [get_buf.c]
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main() {
int sock;
int snd_buf, rcv_buf, state;
socklen_t len;
sock = socket(PF_INET, SOCK_STREAM, 0);
len = sizeof(snd_buf);
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
if (state)
error("getsockopt() error");
len = sizeof(rcv_buf);
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);
if (state)
error("getsockopt() error");
printf("INPUT buffer: %d\n", rcv_buf);
printf("OUTput buffer: %d\n", snd_buf);
close(sock);
return 0;
}
編譯運行:
gcc get_buf.c -o get_buf
./get_buf
運行結果:
INPUT buffer: 131072
OUTPUT buffer: 16384
這是我的輸入輸出緩衝大小
更改
- [set_buf.c]
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[]) {
int sock, state;
int snd_buf = 1024*3;
int rcv_buf = 1024*3;
socklen_t len;
sock = socket(PF_INET, SOCK_STREAM, 0);
len = sizeof(rcv_buf);
state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, sizeof(rcv_buf));
if (state)
error("setsockopt() 1 error");
len = sizeof(snd_buf);
state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, sizeof(snd_buf));
if (state)
error("setsockopt() 2 error");
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &len);
if (state)
error("getsockopt() 1 error");
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &len);
if (state)
error("getsockopt() 2 error");
printf("INPUT :%d\n", rcv_buf);
printf("OUTPUT :%d\n", snd_buf);
close(sock);
return 0;
}
編譯運行:
gcc set_buf.c -o set_buf
./set_buf
運行結果:
INPUT : 6144
OUTPUT : 6144
運行結果跟預想不一樣很正常,因爲緩衝大小設置需要很謹慎,不會完全按我們的要求去改,會自動保證一點空間。
2. SO_REUSEADDR
更改可選項SO_REUSEADDR的狀態,可以決定在Time-wait狀態下的套接字端口號是否能分配給新的套接字。
默認值爲0意味着此狀態下不能分配給新的套接字,改成1即爲可以分配。
2.1 什麼是Time-wait狀態
在套接字斷開連接時,會經歷四次握手無論是調用close() 還是 Ctrl +c
先斷開連接的會先發送FIN消息收到的會開啓time-wait狀態爲了等待判斷自己的ACK是否發送到了,因爲如果自己的沒法送到就關閉套接字了,另一端超時會誤認爲FIN未發送到一直重發,所以就是爲了等待FIN沒有重發來判斷自己的發送成功,所以一般等待時間在3分鐘左右。
實際上,不論是服務端還是客戶端,都要經過一段時間的 Time-wait 過程。先斷開連接的套接字必然會經過 Time-wait 過程,但是由於客戶端套接字的端口是任意制定的,所以無需過多關注 Time-wait 狀態.
這也就是爲什麼斷開連接後立即運行同個斷開會發生地址分配失敗的原因。
2.2 更改可選項SO_REUSEADDR
來判斷關閉後同個端口立即運行新的套接字。
- [reuseadr_eserver.c]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define mx 1024
void error_handling(char *message);
int main(int argc, char* argv[]) {
int ser_sock, cli_sock;
struct sockaddr_in ser_adr, cli_adr;
socklen_t cli_adr_sz;
int state, str_len;
char message[mx];
ser_sock = socket(PF_INET, SOCK_STREAM, 0);
if (ser_sock == -1)
error_handling("socket() error");
int option;
int optlen = sizeof(option);
option = 1;
state = setsockopt(ser_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, optlen);
if (state)
error_handling("setsockopt() error");
memset(&ser_adr, 0, sizeof(ser_adr));
ser_adr.sin_family = AF_INET;
ser_adr.sin_addr.s_addr = htonl(INADDR_ANY);
ser_adr.sin_port = htons(atoi(argv[1]));
if (bind(ser_sock, (struct sockaddr*)&ser_adr, sizeof(ser_adr)) == -1)
error_handling("bind() error");
if (listen(ser_sock, 5) == -1)
error_handling("listen() error");
cli_adr_sz = sizeof(cli_adr);
cli_sock = accept(ser_sock, (struct sockaddr*)&cli_adr, &cli_adr_sz);
if (cli_sock == -1)
error_handling("socket() error_cli");
while ((str_len = read(cli_sock, message, sizeof(message))) != 0) {
write(cli_sock, message, str_len);
// write(1, message, str_len);
}
// shutdown(ser_sock, SHUT_WR);
close(cli_sock);
close(ser_sock);
return 0;
}
void error_handling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
編譯運行:
gcc reuseadr_eserver.c -o reuseadr_eserver
./reuseadr_eserver 9190
^c
./reuseadr_eserver 9190
運行結果:
無報錯
現象:客戶端在服務端關閉以後還能用一下才會關閉,服務端關閉後還可以在客戶端關閉之前重連。
3. TCP_NODELAY
設置Nagle算法使用狀態。
3.1 什麼是Nagle算法
Nagle 算法就是只有在接收到前一個數據的回覆ACK後纔會發送下一個數據,使用與發送短消息時可以提高網絡傳輸效率。
因爲它能在等待的時候將跟多的數據傳入緩衝區下次發送更大的數據,從而減少數據包的使用,每個數據包都會有頭信息,減少使用也就能減少網絡流量。
這隻適用於小文件傳輸,而大文件傳輸的傳入輸出緩衝不會花費太多時間,也會在裝滿數據緩衝包時進行傳輸,所以不會增加數據包的使用,因此,如果不用Nagle算法能夠大大提高效率。
3.2 禁用Nagle算法
只需將TCP_NODELAY 改爲1 (真)即可
int opt_val = 1
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*) &opt_val, sizeof(opt_val)); // 設置
socklen_t opt_len = sizeof(opt_val);
getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*) &opt_val, &opt_len ); //查看