閱讀本文需要對level-ip的整體架構有所了解,如果讀者尚未接觸過level-ip,請先閱讀下面文章:
分享一款Linux平臺下的tcp協議棧!超級透徹!
Linux系統中間件的巧妙實現--以用戶空間的tcp協議棧為例
請根據上述文章中的指引獲取leve-ip的全部源碼,並且嘗試在任意Linux發行版本上編譯運行。
著名的TCP三次握手前面已經介紹過常用套接字接口函數,也就是伺服器調用bind、listen 以及 accept 等待客戶端進行連接,而客戶端connect函數主動請求連接伺服器。
客戶在調用函數 connect 前不必非得調用 bind 函數,因為如果需要的話,內核會確定源IP 地址,並按照一定的算法選擇一個臨時埠作為源埠。當客戶端使用tcp套接字進行連接時,調用 connect 函數將激發 TCP 的三次握手過程。如下圖:
TCP三次握手的剖析這裡我們使用的網絡編程模型都是阻塞式的。所謂阻塞式,就是調用發起後不會直接返回,由作業系統內核處理之後才會返回。相對的,還有一種叫做非阻塞式的,暫時先不討論。
下面是具體的過程:
客戶端的協議棧向伺服器端發送了 SYN 包,並告訴伺服器端當前發送序列號 j,客戶端進入 SYNC_SENT 狀態;伺服器端的協議棧收到這個包之後,和客戶端進行 ACK 應答,應答的值為 j+1,表示對 SYN 包 j 的確認,同時伺服器也發送一個 SYN 包,告訴客戶端當前我的發送序列號為 k,伺服器端進入 SYNC_RCVD 狀態;客戶端協議棧收到 ACK 之後,使得應用程式從 connect 調用返回,表示客戶端到伺服器端的單向連接建立成功,客戶端的狀態為 ESTABLISHED,同時客戶端協議棧也會對伺服器端的 SYN 包進行應答,應答數據為 k+1;應答包到達伺服器端後,伺服器端協議棧使得 accept 阻塞調用返回,這個時候伺服器端到客戶端的單向連接也建立成功,伺服器端也進入 ESTABLISHED 狀態。從socket()函數來看CLOSE狀態前面的文章分析了linux系統中間件的實現思路,明白了應用程式中使用的socket()函數,實際上是調用了level-ip協議站中的ipc_socket()函數,該函數的核心是_socket()函數,我們來看一下這個函數,如下:
int _socket(pid_t pid, int domain, int type, int protocol){ struct socket *sock; struct net_family *family;
if ((sock = alloc_socket(pid)) == NULL) { print_err("Could not alloc socket\n"); return -1; } ... family = families[domain];
if (!family) { print_err("Domain not supported: %d\n", domain); goto abort_socket; } if (family->create(sock, protocol) != 0) { print_err("Creating domain failed\n"); goto abort_socket; } ...}在_socket()函數裡面,我們是調用alloc_socket)()函數來分配一個管理套接字的結構體sock,在該結構體裡面保存了套接字初始狀態和應用程式的相關信息,如下圖:
static struct socket *alloc_socket(pid_t pid){ // TODO: Figure out a way to not shadow kernel file descriptors. // Now, we'll just expect the fds for a process to never exceed this. static int fd = 4097; struct socket *sock = malloc(sizeof (struct socket)); list_init(&sock->list);
sock->pid = pid; sock->refcnt = 1;
pthread_rwlock_wrlock(&slock); sock->fd = fd++; pthread_rwlock_unlock(&slock);
sock->state = SS_UNCONNECTED; sock->ops = NULL; sock->flags = O_RDWR; wait_init(&sock->sleep); pthread_rwlock_init(&sock->lock, NULL); return sock;}在第16行,設置了該套接字的初始狀態為SS_UNCONNECTED,注意此處並不是指tcp的SYNC_SENT狀態,大家不要把它們相混淆。
接著在_socket()函數裡面調用了family->create()函數,這裡考慮到多種協議族的支持,通過函數指針做了一個代碼分離。該函數指針真正指向的函數為inet_create()函數,我們來分析一下它:
int inet_create(struct socket *sock, int protocol){ struct sock *sk; struct sock_type *skt = NULL;
for (int i = 0; i < INET_OPS; i++) { if (inet_ops[i].type & sock->type) { skt = &inet_ops[i]; break; } }
if (!skt) { print_err("Could not find socktype for socket\n"); return 1; }
sock->ops = skt->sock_ops;
sk = sk_alloc(skt->net_ops, protocol); sk->protocol = protocol; sock_init_data(sock, sk); return 0;}第6~10行:獲取tcp連接的相關操作接口集合inet_ops,它裡面又細分出tcp操作接口集合tcp_ops和乙太網底層操作接口集合inet_stream_ops。如下:
static struct sock_type inet_ops[] = { { .sock_ops = &inet_stream_ops, .net_ops = &tcp_ops, .type = SOCK_STREAM, .protocol = IPPROTO_TCP, }};第29行:調用sk_alloc函數分配sk結構體,在該函數裡面調用了tcp_ops接口裡面的tcp_alloc_sock()函數來完成分配工作,如下:
struct sock *sk_alloc(struct net_ops *ops, int protocol){ struct sock *sk; sk = ops->alloc_sock(protocol); sk->ops = ops; return sk;}第4行:調用tcp_ops中的tcp_alloc_sock()函數,產生管理tcp通信的接=結構體sk
第5行:把tcp操作接口集合tcp_ops記錄在sk->ops中
其中,tcp_alloc_sock()函數的實現如下:
struct sock *tcp_alloc_sock(){ struct tcp_sock *tsk = malloc(sizeof(struct tcp_sock));
memset(tsk, 0, sizeof(struct tcp_sock)); tsk->sk.state = TCP_CLOSE; tsk->sackok = 1; tsk->rmss = 1460; // Default to 536 as per spec tsk->smss = 536;
skb_queue_init(&tsk->ofo_queue); return (struct sock *)tsk;}第3~5行:使用malloc函數動態申請內存,並初始化內存值為0
第6行:初始化tcp的連接狀態為close,終於看到tcp的初始連接狀態了!!!
第13行:初始化tcp無序隊列,當tcp接收到的數據不是有序的時候,先把數據掛載在這個隊列上。
從connect()函數來看SYNC_SENT狀態同理,應用程式中的connect()函數,實際上是調用了level-ip協議站中的ipc_connect()函數,該函數的核心是_connect()函數,我們來看一下這個函數,如下:
int _connect(pid_t pid, int sockfd, const struct sockaddr *addr, socklen_t addrlen){ struct socket *sock;
if ((sock = get_socket(pid, sockfd)) == NULL) { print_err("Connect: could not find socket (fd %u) for connection (pid %d)\n", sockfd, pid); return -EBADF; }
socket_wr_acquire(sock);
int rc = sock->ops->connect(sock, addr, addrlen, 0); switch (rc) { case -EINVAL: case -EAFNOSUPPORT: case -ECONNREFUSED: case -ETIMEDOUT: socket_release(sock); socket_free(sock); break; default: socket_release(sock); } return rc;}第5行:獲取_socket()函數申請到的套接字結構體sock,以進一步操作該次tcp連結。
第12行:調用乙太網底層接口inet_stream_connect,該函數實現如下:
static int inet_stream_connect(struct socket *sock, const struct sockaddr *addr, int addr_len, int flags){ struct sock *sk = sock->sk; int rc = 0; ... switch (sock->state) { default: sk->err = -EINVAL; goto out; case SS_CONNECTED: sk->err = -EISCONN; goto out; case SS_CONNECTING: sk->err = -EALREADY; goto out; case SS_UNCONNECTED: sk->err = -EISCONN; if (sk->state != TCP_CLOSE) { goto out; }
sk->ops->connect(sk, addr, addr_len, flags); sock->state = SS_CONNECTING; sk->err = -EINPROGRESS;
if (sock->flags & O_NONBLOCK) { goto out; }
pthread_mutex_lock(&sock->sleep.lock); while (sock->state == SS_CONNECTING && sk->err == -EINPROGRESS) { socket_release(sock); wait_sleep(&sock->sleep); socket_wr_acquire(sock); } pthread_mutex_unlock(&sock->sleep.lock); socket_wr_acquire(sock); switch (sk->err) { case -ETIMEDOUT: case -ECONNREFUSED: goto sock_error; }
if (sk->err != 0) { goto out; }
sock->state = SS_CONNECTED; break; }...
}第7~21行,判斷套接字狀態是否正常,前面我們已經通過_socket()函數把套接字狀態初始化為SS_UNCONNECTED了,因此這裡將進入最後一個分支來執行代碼。
第23行:調用tcp操作接口集合tcp_ops中的tcp_v4_connect函數,在該函數中會構建tcp數據幀,然後調用ip數據幀發送接口來進行數據的發送。
第34行:發送握手幀之後,線程進行睡眠狀態,直到伺服器返回應答幀之後再喚醒。
其中,tcp_v4_connect()調用了tcp_connect()函數,實現如下:
int tcp_connect(struct sock *sk){ struct tcp_sock *tsk = tcp_sk(sk); struct tcb *tcb = &tsk->tcb; int rc = 0; tsk->tcp_header_len = sizeof(struct tcphdr); tcb->iss = generate_iss(); tcb->snd_wnd = 0; tcb->snd_wl1 = 0;
tcb->snd_una = tcb->iss; tcb->snd_up = tcb->iss; tcb->snd_nxt = tcb->iss; tcb->rcv_nxt = 0;
tcp_select_initial_window(&tsk->tcb.rcv_wnd);
rc = tcp_send_syn(sk); tcb->snd_nxt++; return rc;}第8行、第14行:隨機產生了一個序列號,填充到tcp首部中。
第18行:進一步調用tcp_send_syn()函數,該函數實現如下:
static int tcp_send_syn(struct sock *sk){ if (sk->state != TCP_SYN_SENT && sk->state != TCP_CLOSE && sk->state != TCP_LISTEN) { print_err("Socket was not in correct state (closed or listen)\n"); return 1; }
struct sk_buff *skb; struct tcphdr *th; struct tcp_options opts = { 0 }; int tcp_options_len = 0;
tcp_options_len = tcp_syn_options(sk, &opts); skb = tcp_alloc_skb(tcp_options_len, 0); th = tcp_hdr(skb);
tcp_write_syn_options(th, &opts, tcp_options_len); sk->state = TCP_SYN_SENT; th->syn = 1;
return tcp_queue_transmit_skb(sk, skb);}第18行:此時tcp的連接狀態已經變為TCP_SYN_SENT狀態了。
第19行:數據幀中握手標誌置1
第21行:發送tcp握手幀
總結暫時先分析握手的第一個狀態變化,後面繼續把剩餘部分分析完,今天先到這裡。