Unix 网络编程(四)- 典型TCP客服服务器程序开发实例及基本套接字API介绍
轉載:http://blog.csdn.net/michael_kong_nju/article/details/43457393
寫在開頭:
在上一節中我們學習了一些基礎的用來支持網絡編程的API,包括“套接字的地址結構”、“字節排序函數”等。這些API幾乎是所有的網絡編程中都會使用的一些,對于我們正確的編寫網絡程序有很大的作用。在本節中我們會介紹編寫一個基于TCP的套接字程序需要的一些API,同時會介紹一個完整的TCP客戶服務器程序,雖然這個程序功能相對簡單,但確包含了一個客戶服務器程序所有的步驟,一些復雜的程序也都是在此基礎上進行擴充。在后面隨著學習的深入,我們會給這個程序添加功能。
下面我們首先給出這個程序實例,然后根據程序分析其中用到的套接字函數,這些套接字函數也是其他的TCP網絡編程中都會使用到的,包括像:socket 函數,connect 函數,bind 函數,listen 函數,accept函數,fork和exec函數等,其實在之前(一)中已經使用了,而且也有了部分的介紹,這里將會給出詳細的說明 ?。
------------------------------------------------------------------------------------------------------------------------------
TCP客戶服務器程序
我們這里的服務器程序是一個回射服務器,實現以下功能:
(1) 客戶從標準輸入中讀入一行文本,然后將文本寫給服務器;
(2) 服務器從網絡輸入讀入這行文本,并回射給用戶;
(3) 客戶從網絡輸入中讀入這行文本,并顯示在標準輸出中。?
功能的模型如下面所示:
下面是具體的服務器端和客戶端的程序,可以在我們所下載的源碼tcpcliserv/tcpcli01.c 和tcpcliserv/tcpserv01.c中找到,但是為了讓大家更直觀的看到最原始的樣子,這里重寫
了Richard老先生的代碼,你可以直接拷貝,并用gcc編譯然后在你機器上運行。
下面是服務器端代碼 ?echo_server.c
通過創建子進程來處理客戶端的請求從而實現服務器的并發。服務器會調用下面的str_echo的函數,他將客戶端發送過來的內容按原樣返回。
[cpp]?view plaincopy print?
下面是客戶端的程序,echo_tcp_client.c
它發起和服務器連接,然后從標準輸入中讀入數據然后通過socket發送給服務器,并讀取從socket回射的程序。
[cpp]?view plaincopy print?
編譯兩個程序:
[sql]?view plaincopy print?
然后在兩個進程中分別將服務器和客戶端運行起來,如下所示在客戶端可以看到我們輸入一行之后按回車會顯示相同的從服務器端傳回來的文本。
--------------------------------------------------------------------------------------------------------------------------
至此這個程序運行起來了,下面我們開始介紹服務器程序和客戶端程序中的套接字函數是怎樣將整個功能完成的,這里的函數包括:socket 函數,connect 函數,bind 函數,listen 函數,accept函數,fork和exec函數?等。首先我們給出整個函數被調用的一個流程圖,這個流程圖是根據tcp協議建立起來的:
這就是整個函數被調用過程的一個流程以及完成的功能。下面我們詳細的介紹這些函數的用法:
socket 函數
socket 函數是進程執行網絡I/O操作第一件需要做的事情,通過調用socket 函數來指定期望的通信協議類型并返回一個套接字描述符用來標識這個連接,套接字描述符,簡稱sockfd,是一個小的非負整數值類似于文件描述符。用法:
[cpp]?view plaincopy print?例如上面服務器端程序中的
?listenfd?= socket(AF_INET,?SOCK_STREAM,?0); ?
family: 代表的是協議族,指明該套接字在網絡層使用什么來輸出,包括:AF_INET(IPv4), AF_INET6(IPv6), ?AF_LOCAL(Unix 域協議), AF_ROUTE(路由套接字), AF_KEY(秘鑰套接字)等
type: 指明套接字使用的數據流的類型,包括?SOCK_STREAM(字節流套接字), SOCK_DGRAM (數據報套接字),SOCK_SEQPACKET(有序分組套接字), SOCK_RAW(原始套接字)等;
protocol: ?指明的套接字使用的傳輸層協議類型,包括:IPPROTO_TCP(TCP傳輸協議)?IPPROTO_UDP(UDP傳輸協議),IPPROTO_SCTP(SCTP傳輸協議等);一般為了省事直接將這個字段置0,由給定的family和type來決定使用什么協議。
connect函數
connect 函數是客戶端用來和服務器建立連接使用的。調用connect 函數,會引起TCP三次握手的建立。下面是具體的API
[cpp]?view plaincopy print?socketfd?就是socket 函數返回的那個套接字描述符用來表示這個連接;
*servaddr?是一個指向y要連接的服務器的套接字地址結構的指針,這里需要強制類型轉換成通用地址結構,在上一節(三)中我們講過這個套接字地址結構的幾個類型;
addrlen?是這個地址的大小;
如上面事例中
connect(sockfd,?(SA?*)?&servaddr,?sizeof(servaddr));
connect被調用時大概會發生如下幾種情況:
(1) 如果目標地址可達,并運行了服務器程序,那么就正常返回,不會出錯;
(2) 如果目標地址可達,但是沒有運行服務器程序,那么出錯返回:?connect error: Connection Refused;
(3) 如果目標地址在同一個網絡,但是不可達,那么出錯返回:?connect error: connection timed out;(大概是75s之后返回這個錯誤)
(4) 如果目標地址不在同一個網絡,而且無法路由,那么直接返回:?connect error: No route to host.
bind函數
bind 函數是給一個socket 綁定一個套接字地址結構(或者更準確的是:將一個本地協議地址賦予一個套接字),在這個套接字地址結構中有使用的協議、ip、端口號等。如上面程序中:
這里是服務器調用Bind函數進行綁定,客戶端也可以調用bind函數,但是不是很必要。它的API是:
[cpp]?view plaincopy print?對于客戶端,調用這個函數是告訴服務器原地址和端口號是什么;
對于服務器,調用這個函數表名自己只會接受以這個ip地址和端口號為目的的客戶端請求。
不過,他們都可以將這個地址設為通配地址(INADDR_ANY)端口號設為(0),這樣就可以接受所有客戶端的請求并且允許內核選擇源ip地址和分配臨時端口。
listen函數
listen 函數是服務器端調用的函數。從宏觀上講,listen發生在服務器端socket, bind函數之后,accept函數之前,調用它表明服務器端在監聽來自客戶端的請求。從細節的角度來講,listen函數被調用之后,服務器端開始維護兩個隊列:一個隊列稱為未完成隊列是剛監聽到用戶發起的連接請求組成的隊列(接收到SYN),即三次握手的第一階段,這個時候將這個socket 請求放在這個隊列中;另一個隊列稱為已完成隊列,是從未完成隊列中將完成三次握手的socket調入的。具體的API是:
[cpp]?view plaincopy print?這里的?backlog?沒有確切的解釋,通常認為是這兩個隊列中條目之和。但是這個值一般都會乘上一個模糊因子這里是1.5來規定最大。歷史上這個值一般是5,現在因為服務器繁忙會取一個比較大的值;
accept 函數
accept 函數可以緊接著上面的listen函數討論,accpet 函數被調用時將會從listen狀態中的已完成連接套接字隊列中選擇隊首進行服務。如果隊列為空,那么將阻塞。下面是它的API:
[cpp]?view plaincopy print?如上面服務器端程序所示:
?connfd?= accept(listenfd,?(SA?*)?&cliaddr,?&clilen); ?
其中:sockfd 是監聽套接字的描述符, 第二個參數是這次連接的對端的協議地址,第三個參數是長度,這里是引用的形式,因為要往里面寫數據。如果沒必要得到這個地址,可以直接置0.
注意這個函數的返回值稱為?連接套接字描述符, 和socket 返回的一次服務器進程中只創建一次的監聽套接字描述符不同,這里每次調用accept 函數都會返回這么一個連接套接字描述符,然后對此描述符進行處理。
并發服務器 fork/exec函數
(一)中的服務器程序是一個簡單的迭代的服務器程序,當accept一個客戶請求之后服務器便一直為這個程序服務,對于這種簡單的獲取時間的程序來講是可以的,但是有些服務器程序執行的操作需要花費很長時間,而且我們又不希望服務器一直在處理這么一個客戶請求,所以就希望編寫并發的服務器程序,使得服務器同時可以處理多個請求,而編寫并發服務器最簡單的方法就是fork 一個子進程來服務每個客戶,我們上面的程序也是采用這種方式:
下面是fork()函數的具體用法:
[cpp]?view plaincopy print?這個函數也是我們迄今為止見過的為數不多的兩個有兩個返回值的函數,因為子進程調用getppid()函數可以獲得父進程的id,所以在子進程中其返回值就直接是0了,而父進程因為要管理所有的子進程,所以就在父進程的返回值中拿到這個值;
注意,父進程和子進程共享在創建這個子進程之前的所有描述符。所以這里的connfd才可以在子進程中被引用,而且描述符的引用數會將1,所以當父進程close(connfd)的時候只會減1,只有子進程也close才會減為0;
getsockname 和 getpeername 函數
在一開始的事例程序中并沒有這兩個函數的影子,但是在后面的程序中,可能會用到這兩個函數,所以這里有必要說明一下。
[cpp]?view plaincopy print?
getsockname ()用來返回與sockfd這個套接字關聯的本地協議組地址,通過這個地址可以查看內核賦予的ip地址和端口號,一般用于客戶端不適用Bind函數而直接調用socket從而由內核決定本地ip地址和端口號是什么,這個時候用這個函數查看很有用;
getpeername()一般用于服務器在fork一個子進程處理一個客戶端的請求時,而子進程內存映像因為被執行的具體程序覆蓋而丟失了客戶的協議地址,這個時候通過調用getpeername()函數可以重新獲得。
我們將在后面碰到這兩個函數的的程序中再討論這兩個函數。
總結:
我們在篇博文中首先給出了一個并發的典型的TCP客戶服務程序,并運行了這個程序。之后我們從TCP協議的角度給出了每一個函數完成的功能,最后詳細的分析這些函數的API。所有的客戶和服務器程序都從socket開始,它返回一個套接字描述符,對于服務器而言返回的是監聽套接字描述符。客戶之后調用connect進行連接,內核發送三次握手,之后服務器調用bind, listen, 和accept函數等。accept之后就開始處理一個請求,這里講解了通過調用fork()函數創建子進程,由子進程并發的調度。
2015/02/03 ?于南京 CSDN 如需轉載請注明地址謝謝:http://blog.csdn.net/michael_kong_nju/article/details/43457393
總結
以上是生活随笔為你收集整理的Unix 网络编程(四)- 典型TCP客服服务器程序开发实例及基本套接字API介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 最佳女配剧情介绍
- 下一篇: Unix网络编程(六)高级I/O技术之复