天天看點

基于TCP協定的網絡程式

下圖是基于TCP協定的用戶端/伺服器程式的一般流程:

圖1.1 TCP協定通訊流程

<a href="http://s4.51cto.com/wyfs02/M01/85/88/wKioL1en6e6A3HjZAAFHuY9TmLk995.png" target="_blank"></a>

建立連結的過程:

圖1.2  建立連接配接的過程

<a href="http://s5.51cto.com/wyfs02/M00/85/88/wKioL1en6hqDOmzlAACZ3XO_LCQ559.png" target="_blank"></a>

伺服器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處于監聽端口的狀态,用戶端調用 socket()初始化後,調用connect()發出SYN段并阻塞等待伺服器應答,伺服器應答一個SYN-ACK段,用戶端收到後從 connect()傳回,同時應答一個ACK段,伺服器收到後從accept()傳回。

資料傳輸的過程:

建立連接配接後,TCP 協定提供全雙工的通信服務,但是一般的用戶端/伺服器程式的流程是由用戶端主動發起請求,伺服器被動處理請求,一問一答的方式。是以,伺服器從 accept()傳回後立刻調用read(),讀socket就像讀管道一樣,如果沒有資料到達就阻塞等待,這時用戶端調用write()發送請求給服務 器,伺服器收到後從read()傳回,對用戶端的請求進行處理,在此期間用戶端調用read()阻塞等待伺服器的應答,伺服器調用write()将處理結 果發回給用戶端,再次調用read()阻塞等待下一條請求,用戶端收到後從read()傳回,發送下一條請求,如此循環下去。

關閉連結的過程:

圖1.3 關閉連接配接的過程

如果用戶端沒有更多的請求了,就調用close()關閉連接配接,就像寫端關閉的管道一樣,伺服器的read()傳回0,這樣伺服器就知道用戶端關閉了 連接配接,也調用close()關閉連接配接。注意,任何一方調用close()後,連接配接的兩個傳輸方向都關閉,不能再發送資料了。如果一方調用 shutdown()則連接配接處于半關閉狀态,仍可接收對方發來的資料。

在學習socket API時要注意應用程式和TCP協定層是如何互動的: *應用程式調用某個socket函數時TCP協定層完成什麼動作,比如調用connect()會發出SYN段 *應用程式如何知道TCP協定層的狀态變化,比如從某個阻塞的socket函數傳回就表明TCP協定收到了某些段,再比如read()傳回0就表明收到了FIN段

先看一下需要用到的函數

<code>NAME</code>

<code>       </code><code>socket - create an endpoint </code><code>for</code> <code>communication</code>

<code>SYNOPSIS</code>

<code>       </code><code>#include &lt;sys/types.h&gt;          </code><code>/* See NOTES */</code>

<code>       </code><code>#include &lt;sys/socket.h&gt;</code>

<code>       </code><code>int</code> <code>socket(</code><code>int</code> <code>domain, </code><code>int</code> <code>type, </code><code>int</code> <code>protocol);</code>

<code>DESCRIPTION</code>

<code>       </code><code>socket() creates an endpoint </code><code>for</code> <code>communication and returns a descriptor.</code>

socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣傳回一個檔案描述符,應用程式可以像讀寫檔案一樣用 read/write在網絡上收發資料,如果socket()調用出錯則傳回-1。對于IPv4,family參數指定為AF_INET。對于TCP協 議,type參數指定為SOCK_STREAM,表示面向流的傳輸協定。如果是UDP協定,則type參數指定為SOCK_DGRAM,表示面向資料報的 傳輸協定。protocol參數的介紹從略,指定為0即可。

<code>       </code><code>bind - bind a name to a socket</code>

<code>       </code><code>int</code> <code>bind(</code><code>int</code> <code>sockfd, </code><code>const</code> <code>struct</code> <code>sockaddr *addr,</code>

<code>                </code><code>socklen_t addrlen);</code>

伺服器程式所監聽的網絡位址和端口号通常是固定不變的,用戶端程式得知伺服器程式的位址和端口号後就可以向伺服器發起連接配接,是以伺服器需要調用bind綁定一個固定的網絡位址和端口号。bind()成功傳回0,失敗傳回-1。

bind() 的作用是将參數sockfd和myaddr綁定在一起,使sockfd這個用于網絡通訊的檔案描述符監聽myaddr所描述的位址和端口号。前面講 過,struct sockaddr *是一個通用指針類型,myaddr參數實際上可以接受多種協定的sockaddr結構體,而它們的長度各不相同,是以需要第三個參數addrlen指定 結構體的長度。我們的程式中對myaddr參數是這樣初始化的:

<code>bzero(&amp;servaddr, </code><code>sizeof</code><code>(servaddr));</code>

<code>servaddr.sin_family = AF_INET;</code>

<code>servaddr.sin_addr.s_addr = htonl(INADDR_ANY);</code>

<code>servaddr.sin_port = htons(SERV_PORT);</code>

首先将整個結構體清零,然後設定位址類型為AF_INET,網絡位址為INADDR_ANY,這個宏表示本地的任意IP位址,因為伺服器可能有多個網卡, 每個網卡也可能綁定多個IP位址,這樣設定可以在所有的IP位址上監聽,直到與某個用戶端建立了連接配接時才确定下來到底用哪個IP位址,端口号為 SERV_PORT,我們定義為8000。

<code>       </code><code>listen - listen </code><code>for</code> <code>connections on a socket</code>

<code>       </code><code>int</code> <code>listen(</code><code>int</code> <code>sockfd, </code><code>int</code> <code>backlog);</code>

典型的伺服器程式可以同時服務于多個用戶端,當有用戶端發起連接配接時,伺服器調用的accept()傳回并接受這個連接配接,如果有大量的用戶端發起連接配接而服務 器來不及處理,尚未accept的用戶端就處于連接配接等待狀态,listen()聲明sockfd處于監聽狀态,并且最多允許有backlog個用戶端處于 連接配接待狀态,如果接收到更多的連接配接請求就忽略。listen()成功傳回0,失敗傳回-1。

<code>       </code><code>int</code> <code>accept(</code><code>int</code> <code>sockfd, </code><code>struct</code> <code>sockaddr *addr, socklen_t *addrlen);</code>

三方握手完成後,伺服器調用accept()接受連接配接,如果伺服器調用accept()時還沒有用戶端的連接配接請求,就阻塞等待直到有用戶端連接配接上來。 cliaddr是一個傳出參數,accept()傳回時傳出用戶端的位址和端口号。addrlen參數是一個傳入傳出參數(value-result argument),傳入的是調用者提供的緩沖區cliaddr的長度以避免緩沖區溢出問題,傳出的是用戶端位址結構體的實際長度(有可能沒有占滿調用者 提供的緩沖區)。如果給cliaddr參數傳NULL,表示不關心用戶端的位址。

伺服器使這樣子的:

<code>while</code> <code>(1) {</code>

<code>    </code><code>cliaddr_len = </code><code>sizeof</code><code>(cliaddr);</code>

<code>    </code><code>connfd = accept(listenfd, </code>

<code>            </code><code>(</code><code>struct</code> <code>sockaddr *)&amp;cliaddr, &amp;cliaddr_len);</code>

<code>    </code><code>n = read(connfd, buf, MAXLINE);</code>

<code>    </code><code>......</code>

<code>    </code><code>close(connfd);</code>

<code>}</code>

整個是一個while死循環,每次循環處理一個用戶端連接配接。由于cliaddr_len是傳入傳出參數,每次調用accept()之前應該重新賦初值。 accept()的參數listenfd是先前的監聽檔案描述符,而accept()的傳回值是另外一個檔案描述符connfd,之後與用戶端之間就通過 這個connfd通訊,最後關閉connfd斷開連接配接,而不關閉listenfd,再次回到循環開頭listenfd仍然用作accept的參數。 accept()成功傳回一個檔案描述符,出錯傳回-1。

TCP網絡程式:

server.c的作用是從用戶端讀字元,然後将每個字元轉換為大寫并回送給用戶端。

/*server.c*/

<code>#include &lt;stdio.h&gt;</code>

<code>#include &lt;stdlib.h&gt;</code>

<code>#include &lt;string.h&gt;</code>

<code>#include &lt;unistd.h&gt;</code>

<code>#include &lt;sys/socket.h&gt;</code>

<code>#include &lt;netinet/in.h&gt;</code>

<code>#define MAXLINE 80</code>

<code>#define SERV_PORT 8000</code>

<code>int</code> <code>main(</code><code>void</code><code>)</code>

<code>{</code>

<code>    </code><code>struct</code> <code>sockaddr_in servaddr, cliaddr;</code><code>//定義套接字位址</code>

<code>    </code><code>socklen_t cliaddr_len;</code>

<code>    </code><code>int</code> <code>listenfd, connfd;</code>

<code>    </code><code>char</code> <code>buf[MAXLINE];</code>

<code>    </code><code>char</code> <code>str[INET_ADDRSTRLEN];</code>

<code>    </code><code>int</code> <code>i, n;</code>

<code>    </code><code>listenfd = socket(AF_INET, SOCK_STREAM, 0);</code>

<code>    </code><code>bzero(&amp;servaddr, </code><code>sizeof</code><code>(servaddr)); </code><code>//結構體初始化</code>

<code>    </code><code>servaddr.sin_family = AF_INET;</code>

<code>    </code><code>servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  </code><code>//INADDR_ANY 宏</code>

<code>    </code><code>servaddr.sin_port = htons(SERV_PORT); </code><code>//端口号</code>

<code>    </code> 

<code>    </code><code>bind(listenfd, (</code><code>struct</code> <code>sockaddr *)&amp;servaddr, </code><code>sizeof</code><code>(servaddr));</code>

<code>    </code><code>listen(listenfd, 20);</code>

<code>    </code><code>printf</code><code>(</code><code>"Accepting connections ...\n"</code><code>);</code>

<code>    </code><code>while</code> <code>(1) {</code>

<code>        </code><code>cliaddr_len = </code><code>sizeof</code><code>(cliaddr);</code>

<code>        </code><code>connfd = accept(listenfd, </code>

<code>                </code><code>(</code><code>struct</code> <code>sockaddr *)&amp;cliaddr, &amp;cliaddr_len);</code>

<code>      </code><code>//accept()傳回時傳出用戶端的位址和端口号</code>

<code>      </code> 

<code>        </code><code>n = read(connfd, buf, MAXLINE);</code>

<code>        </code><code>printf</code><code>(</code><code>"received from %s at PORT %d\n"</code><code>,</code>

<code>               </code><code>inet_ntop(AF_INET, &amp;cliaddr.sin_addr, str, </code><code>sizeof</code><code>(str)),</code>

<code>               </code><code>ntohs(cliaddr.sin_port));</code>

<code>        </code><code>for</code> <code>(i = 0; i &lt; n; i++)</code>

<code>            </code><code>buf[i] = </code><code>toupper</code><code>(buf[i]);</code><code>//用來将字元c轉換為大寫英文字母</code>

<code>        </code><code>write(connfd, buf, n);</code>

<code>        </code><code>close(connfd);</code>

<code>    </code><code>}</code>

由于用戶端不需要固定的端口号,是以不必調用bind(),用戶端的端口号由核心自動配置設定。注意,用戶端不是不允許調用bind(),隻是沒有必要 調用bind()固定一個端口号,伺服器也不是必須調用bind(),但如果伺服器不調用bind(),核心會自動給伺服器配置設定監聽端口,每次啟動伺服器 時端口号都不一樣,用戶端要連接配接伺服器就會遇到麻煩。

<code>       </code><code>connect - initiate a connection on a socket</code>

<code>       </code><code>int</code> <code>connect(</code><code>int</code> <code>sockfd, </code><code>const</code> <code>struct</code> <code>sockaddr *addr,</code>

<code>                   </code><code>socklen_t addrlen);</code>

用戶端需要調用connect()連接配接伺服器,connect和bind的參數形式一緻,差別在于bind的參數是自己的位址,而connect的參數是對方的位址。connect()成功傳回0,出錯傳回-1。

client.c的作用是從指令行參數中獲得一個字元串發給伺服器,然後接收伺服器傳回的字元串并列印。

/*client.c*/

<code>int</code> <code>main(</code><code>int</code> <code>argc, </code><code>char</code> <code>*argv[])</code>

<code>    </code><code>struct</code> <code>sockaddr_in servaddr;</code>

<code>    </code><code>int</code> <code>sockfd, n;</code>

<code>    </code><code>char</code> <code>*str;</code>

<code>    </code><code>if</code> <code>(argc != 2) {</code>

<code>        </code><code>fputs</code><code>(</code><code>"usage: ./client message\n"</code><code>, stderr);</code>

<code>        </code><code>exit</code><code>(1);</code>

<code>    </code><code>str = argv[1];</code>

<code>    </code><code>sockfd = socket(AF_INET, SOCK_STREAM, 0);</code>

<code>    </code><code>bzero(&amp;servaddr, </code><code>sizeof</code><code>(servaddr));</code>

<code>    </code><code>inet_pton(AF_INET, </code><code>"127.0.0.1"</code><code>, &amp;servaddr.sin_addr);</code>

<code>    </code><code>servaddr.sin_port = htons(SERV_PORT);</code>

<code>    </code><code>connect(sockfd, (</code><code>struct</code> <code>sockaddr *)&amp;servaddr, </code><code>sizeof</code><code>(servaddr));</code>

<code>    </code><code>write(sockfd, str, </code><code>strlen</code><code>(str));</code>

<code>    </code><code>n = read(sockfd, buf, MAXLINE);</code>

<code>    </code><code>printf</code><code>(</code><code>"Response from server:\n"</code><code>);</code>

<code>    </code><code>write(STDOUT_FILENO, buf, n);</code>

<code>    </code><code>close(sockfd);</code>

<code>    </code><code>return</code> <code>0;</code>

打開兩個終端,依次運作./server和./client tcp

<a href="http://s1.51cto.com/wyfs02/M02/85/88/wKioL1en7UmQteD1AACgiKOMgVY980.png" target="_blank"></a>

本文轉自 七十七快 51CTO部落格,原文連結:http://blog.51cto.com/10324228/1835531

繼續閱讀