寫在前面:
版本信息:
Linux內核2.6.24(大部分centos、ubuntu應該都在3.1+。但是2.6的版本比較穩定,后續版本本質變化也不是很大)
ipv4 協議
https://blog.csdn.net/ComplexMaze/article/details/124201088
本文使用案例如上地址,感謝案例的分享,本篇文章核心部分還是在Linux內核源碼分析~
為什么寫下這篇文章,因為在實際項目中,是無法避免TCP通訊(對于這點,可能大部分Java程序員感受不到底層的網絡通訊),正因為無法避免TCP通訊,恰好TCP通訊存在三次握手和四次揮手的過程,如果建立一次連接就三次握手和四次揮手,而我們清楚的知道三次握手和四次揮手是同步的過程,此過程也會帶來不少的時間浪費和資源的浪費。所以Linux內核TCP網絡協議棧就出現了KeepAlive機制,此機制減少三次握手和四次揮手次數,第一次建立連接后保持長連接,后續通訊就可以只考慮發送數據報文即可。往往出現一個機制解決某個問題,其他問題又出現,如果所有連接都建立長連接保活機制,而連接數又有限制,此時該如何解決呢?如下代碼,Linux使用心跳機制去檢測連接是否存活~
#define TCP_KEEPALIVE_TIME (120*60*HZ) // 首次,2小時
#define TCP_KEEPALIVE_PROBES 9 // 重試9次
#define TCP_KEEPALIVE_INTVL (75*HZ) // 后續,每75秒一次
- 在Linux內核中默認關閉KeepAlive
- 開啟KeepAlive后,默認2小時后往對端發送心跳包,檢查是否還活著
- 默認后續每75秒往對端發送心跳包,檢查是否還活著
- 默認當對端9次都沒有響應報文就發送RST報文,斷開TCP連接,釋放資源!
- 當然這一切參數都可以配置,通過sys_setsockopt系統調用,當然setsockopt函數庫就行啦
回到上述描述的話題,往往出現一個機制解決某個問題,其他問題又出現。解決了頻繁握手和揮手的時間,但是連接數量不夠的問題又出現了,可能很多連接建立在那里,完全不通訊了,或者對端已經斷網,或者宕機等等原因占用連接不釋放,而Linux默認一個連接存活檢測需要2個小時+ 才去檢測對端是否活著,如果說服務器的負荷比較大,2小時才檢測一次,會導致正常請求無法進行,所以此參數需要通過setsockopt函數庫重新設置參數(當然,如果是Java等等虛擬機語言,本身也有自身的封裝函數去操作setsockopt函數庫,或者直接調用sys_setsockopt系統調用,這個需要看語言手冊~!)話又說回來,如果設置的閾值大小、時間太短的問題也會很明顯,一直都在發心跳包檢測,甚至性能損耗大于了握手和揮手的時間,所以需要根據業務環境、服務器的硬件從性能損耗和空閑連接數量做折中考慮~
案例:
下面是C語言的服務端的案例源碼,此案例是借用的,但是我們重點關心機制~
/*server.c*/
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include <netinet/tcp.h>
?
#define PORT 4000//端口號
#define BACKLOG 5/*最大監聽數*/
#define MAX_DATA 100//接收到的數據最大程度
?
int main(){int sockfd,new_fd;/*socket句柄和建立連接后的句柄*/struct sockaddr_in my_addr;/*本方地址信息結構體,下面有具體的屬性賦值*/struct sockaddr_in their_addr;/*對方地址信息*/int sin_size;char buf[MAX_DATA];//儲存接收數據
?sockfd=socket(AF_INET,SOCK_STREAM,0);//建立socket if(sockfd==-1){printf("socket failed:%d",errno);return -1;}my_addr.sin_family=AF_INET;/*該屬性表示接收本機或其他機器傳輸*/my_addr.sin_port=htons(PORT);/*端口號*/my_addr.sin_addr.s_addr=htonl(INADDR_ANY);/*IP,括號內容表示本機IP*/bzero(&(my_addr.sin_zero),8);/*將其他屬性置0*/if(bind(sockfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr))<0){//綁定地址結構體和socketprintf("bind error");return -1;}listen(sockfd,BACKLOG);//開啟監聽 ,第二個參數是最大監聽數 while(1){sin_size=sizeof(struct sockaddr_in);new_fd=accept(sockfd,(struct sockaddr*)&their_addr,&sin_size);//在這里阻塞知道接收到消息,參數分別是socket句柄,接收到的地址信息以及大小 // 開啟保活,1分鐘內探測不到,斷開連接int keep_alive = 1;int keep_idle = 3;int keep_interval = 1;int keep_count = 57;if (setsockopt(new_fd, SOL_SOCKET, SO_KEEPALIVE, &keep_alive, sizeof(keep_alive))) {perror("Error setsockopt(SO_KEEPALIVE) failed");exit(1);}if (setsockopt(new_fd, IPPROTO_TCP, TCP_KEEPIDLE, &keep_idle, sizeof(keep_idle))) {perror("Error setsockopt(TCP_KEEPIDLE) failed");exit(1);}if (setsockopt(new_fd, SOL_TCP, TCP_KEEPINTVL, (void *)&keep_interval, sizeof(keep_interval))) {perror("Error setsockopt(TCP_KEEPINTVL) failed");exit(1);}if (setsockopt(new_fd, SOL_TCP, TCP_KEEPCNT, (void *)&keep_count, sizeof(keep_count))) {perror("Error setsockopt(TCP_KEEPCNT) failed");exit(1);}while(new_fd != -1) {recv(new_fd,buf,MAX_DATA,0);//將接收數據打入buf,參數分別是句柄,儲存處,最大長度,其他信息(設為0即可)。 printf("%s",buf);}}return 0;
}
此服務端案例非常的簡單,當客戶端與服務端建立連接后,修改KeepAlive的機制參數,使用setsockopt庫函數修改。
SO_KEEPALIVE:開啟KeepAlive機制
TCP_KEEPIDLE:首次檢測的時長
TCP_KEEPINTVL:下次檢測的間隔時長
TCP_KEEPCNT:重試閾值次數
源碼分析:
首先看到TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT這三個參數的設置,源碼在net/ipv4/tcp.c 文件do_tcp_setsockopt方法,此方法由sys_setsockopt系統調用方法調用。
static int do_tcp_setsockopt(struct sock *sk, int level,int optname, char __user *optval, int optlen)
{struct tcp_sock *tp = tcp_sk(sk);struct inet_connection_sock *icsk = inet_csk(sk);int val;int err = 0;switch (optname) {…………case TCP_KEEPIDLE: // 設置第一次觸發的時間if (val < 1 || val > MAX_TCP_KEEPIDLE)err = -EINVAL;else {// 算出設置的時間tp->keepalive_time = val * HZ;// 如果KeepAlive機制已開啟,并且當前不是關閉狀態和監聽狀態。if (sock_flag(sk, SOCK_KEEPOPEN) &&!((1 << sk->sk_state) &(TCPF_CLOSE | TCPF_LISTEN))) {// 當前時間 - 上次ACK的時候 = 相對時間__u32 elapsed = tcp_time_stamp - tp->rcv_tstamp;if (tp->keepalive_time > elapsed)// 如果上次ACK同步的時間小于設置的時間,那就把剩余的時間算出來elapsed = tp->keepalive_time - elapsed;else// 如果上次ACK同步的時間大于設置的時間,那就立馬檢測elapsed = 0;// 設置內核的定時器inet_csk_reset_keepalive_timer(sk, elapsed);}}break;case TCP_KEEPINTVL: // 設置每次的間隔時間if (val < 1 || val > MAX_TCP_KEEPINTVL)err = -EINVAL;elsetp->keepalive_intvl = val * HZ;break;case TCP_KEEPCNT: // 設置閾值次數if (val < 1 || val > MAX_TCP_KEEPCNT)err = -EINVAL;elsetp->keepalive_probes = val;break;release_sock(sk);return err;
}
這里非常的簡單,通過switch case的形式把參數添加到結構體中,并且設置了首次觸發的時間
接下來,我們看到定時器何時設置的。在net/ipv4/tcp_ipv4.c 文件中tcp_v4_init_sock方法。
static int tcp_v4_init_sock(struct sock *sk)
{…………tcp_init_xmit_timers(sk);…………return 0;
}void tcp_init_xmit_timers(struct sock *sk)
{inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer,&tcp_keepalive_timer);
}void inet_csk_init_xmit_timers(struct sock *sk,void (*retransmit_handler)(unsigned long),void (*delack_handler)(unsigned long),void (*keepalive_handler)(unsigned long))
{struct inet_connection_sock *icsk = inet_csk(sk);…………// 初始化sk->sk_timer,也即初始化timer_list// timer_list在內核是一個定時器的結構體init_timer(&sk->sk_timer);// 設置定時器的回調函數sk->sk_timer.function = keepalive_handler;…………
}
把大部分無關的代碼省略掉以后,源碼看起來非常的簡單,這里初始化了定時器,并且把定時器的回調函數設置成tcp_keepalive_timer,所以接下來,我們直接分析tcp_keepalive_timer方法即可。在net/ipv4/tcp_timer.c 文件中?tcp_keepalive_timer方法。
// 當達到keepalive設置的值以后回掉此方法。
static void tcp_keepalive_timer (unsigned long data)
{struct sock *sk = (struct sock *) data;struct inet_connection_sock *icsk = inet_csk(sk);struct tcp_sock *tp = tcp_sk(sk);__u32 elapsed;/* Only process if socket is not in use. */bh_lock_sock(sk);if (sock_owned_by_user(sk)) {// 這里很簡單,因為鎖的原因,所以需要重試。inet_csk_reset_keepalive_timer (sk, HZ/20);goto out;}// 4次揮手階段,而此時達到了保活的檢測,此時發送RST報文給對端,表示我要斷開了,然后釋放資源即可。if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {if (tp->linger2 >= 0) {const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;if (tmo > 0) {tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);goto out;}}tcp_send_active_reset(sk, GFP_ATOMIC);goto death;}// 如果KeepAlive沒有開啟,或者當前已經是關閉狀態if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)goto out;// 算出下次檢測的時間elapsed = keepalive_time_when(tp);// 此時正在發送報文,所以無須檢測,直接重置下次檢測的時間if (tp->packets_out || tcp_send_head(sk))goto resched;// 算出距離上一次ACK的相對時間elapsed = tcp_time_stamp - tp->rcv_tstamp;// 如果上一次ACK的相對時間 大于等于 設置的時間,那么就代表達到一次閾值if (elapsed >= keepalive_time_when(tp)) {// 查看是否達到次數閾值,達到閾值后直接發送RST報文給對方,然后關閉連接。if ((!tp->keepalive_probes && icsk->icsk_probes_out >= sysctl_tcp_keepalive_probes) ||(tp->keepalive_probes && icsk->icsk_probes_out >= tp->keepalive_probes)) {tcp_send_active_reset(sk, GFP_ATOMIC);tcp_write_err(sk);goto out;}// 沒達到閾值的情況// 嘗試發送報文給對方,看是否還活著if (tcp_write_wakeup(sk) <= 0) {// 如果回復了,那就把下次檢測的時間設置好icsk->icsk_probes_out++;elapsed = keepalive_intvl_when(tp);} else { // 對端沒有回復,不知道是因為丟失還是怎么了,所以加快速度,嘗試下一次。elapsed = TCP_RESOURCE_PROBE_INTERVAL;}} else {// 沒有達到上次ACK的相對時間,所以算出差值,設置到定時器中。elapsed = keepalive_time_when(tp) - elapsed;}TCP_CHECK_TIMER(sk);sk_stream_mem_reclaim(sk);resched:// 把最新值設置到定時器中。inet_csk_reset_keepalive_timer (sk, elapsed);goto out;death:// 關閉連接,釋放資源。tcp_done(sk);out:bh_unlock_sock(sk);sock_put(sk);
}
此方法是當定時器結束后回調執行,檢測是否達到了我們設置或者默認的閾值,如果沒有達到,再設置下一次定時器的時間,如果達到了就發送RST報文,關閉連接,釋放資源~!