1. 程式人生 > >Socket (一) 基礎及接口函數

Socket (一) 基礎及接口函數

聲明 雙向 之一 限制 log 初始化 路徑名 ipv4 sockfd

  Socket,又稱為套接字,Socket是計算機網絡通信的基本的技術之一。如今大多數基於網絡的軟件,如瀏覽器,即時通訊工具甚至是P2P下載都是基於Socket實現的。本篇會介紹一下基於TCP/IP的Socket編程,並且如何寫一個客戶端/服務器程序。

1.1 背景介紹

  Unix的輸入輸出(IO)系統遵循Open-Read-Write-Close這樣的操作範本。當一個用戶進程進行IO操作之前,它需要調用Open來指定並獲取待操作文件或設備讀取或寫入的權限。一旦IO操作對象被打開,那麽這個用戶進程可以對這個對象進行一次或多次的讀取或寫入操作。Read操作用來從IO操作對象讀取數據,並將數據傳遞給用戶進程。Write操作用來將用戶進程中的數據傳遞(寫入)到IO操作對象。 當所有的Read和Write操作結束之後,用戶進程需要調用Close來通知系統其完成對IO對象的使用。
  在Unix開始支持進程間通信(InterProcess Communication,簡稱IPC)時,IPC的接口就設計得類似文件IO操作接口。在Unix中,一個進程會有一套可以進行讀取寫入的IO描述符。IO描述符可以是文件,設備或者是通信通道(socket套接字)。一個文件描述符由三部分組成:創建(打開socket),讀取寫入數據(接受和發送到socket)還有銷毀(關閉socket)。
  在Unix系統中,類BSD版本的IPC接口是作為TCP和UDP協議之上的一層進行實現的。消息的目的地使用socket地址來表示。一個socket地址是由網絡地址和端口號組成的通信標識符。
  進程間通信操作需要一對兒socket。進程間通信通過在一個進程中的一個socket與另一個進程中得另一個socket進行數據傳輸來完成。當一個消息執行發出後,這個消息在發送端的socket中處於排隊狀態,直到下層的網絡協議將這些消息發送出去。當消息到達接收端的socket後,其也會處於排隊狀態,直到接收端的進程對這條消息進行了接收處理。

1.2 TCP和UDP通信
  關於socket編程我們有兩種通信協議可以進行選擇。一種是數據報通信,另一種就是流通信。
  1.2.1 數據報通信
  數據報通信協議,就是我們常說的UDP(User Data Protocol 用戶數據報協議)。UDP是一種無連接的協議,這就意味著我們每次發送數據報時,需要同時發送本機的socket描述符和接收端的socket描述符。因此,我們在每次通信時都需要發送額外的數據。
  1.2.2 流通信
  流通信協議,也叫做TCP(Transfer Control Protocol,傳輸控制協議)。和UDP不同,TCP是一種基於連接的協議。在使用流通信之前,我們必須在通信的一對兒socket之間建立連接。其中一個socket作為服務器進行監聽連接請求。另一個則作為客戶端進行連接請求。一旦兩個socket建立好了連接,他們可以單向或雙向進行數據傳輸。
  我們進行socket編程使用UDP還是TCP呢。選擇基於何種協議的socket編程取決於你的具體的客戶端-服務器端程序的應用場景。下面我們簡單分析一下TCP和UDP協議的區別:
  在UDP中,每次發送數據報時,需要附帶上本機的socket描述符和接收端的socket描述符。而由於TCP是基於連接的協議,在通信的socket對之間需要在通信之前建立連接,因此會有建立連接這一耗時存在於TCP協議的socket編程。
  在UDP中,數據報數據在大小上有64KB的限制。而TCP中也不存在這樣的限制。一旦TCP通信的socket對建立了連接,他們之間的通信就類似IO流,所有的數據會按照接受時的順序讀取。
  UDP是一種不可靠的協議,發送的數據報不一定會按照其發送順序被接收端的socket接受。然後TCP是一種可靠的協議。接收端收到的包的順序和包在發送端的順序是一致的。
  簡而言之,TCP適合於諸如遠程登錄(rlogin,telnet)和文件傳輸(FTP)這類的網絡服務。因為這些需要傳輸的數據的大小不確定。而UDP相比TCP更加簡單輕量一些。UDP用來實現實時性較高或者丟包不重要的一些服務。在局域網中UDP的丟包率都相對比較低。

1.3 C 中的 Socket 編程

   說白了Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。  

  在使用Socket API編程時,需要重點先了解幾個API,包括:socket()、bind()、connect()、listen()、accept()、send()和recv()、sendto()和recvfrom()、close()和shutdown()、getpeername()、gethostname()。這些接口是在Winsock2.h 中定義的不是在 MFC 中定義的,只需包含 Winsock2.h 頭文件和 Ws2_32.lib 庫就可以了。

   服務器端先初始化Socket,然後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端連接。在這時如果有個客戶端初始化一個Socket,然後連接服務器(connect),如果連接成功,這時客戶端與服務器端的連接就建立了。客戶端發送數據請求,服務器端接收請求並處理請求,然後把回應數據發送給客戶端,客戶端讀取數據,最後關閉連接,一次交互結束。

1.3.1 客戶端/服務端模式:

  在TCP/IP網絡應用中,通信的兩個進程相互作用的主要模式是客戶/服務器模式,即客戶端向服務器發出請求,服務器接收請求後,提供相應的服務。
  服務端:建立socket,聲明自身的端口號和地址並綁定到socket,使用listen打開監聽,然後不斷用accept去查看是否有連接,如果有,捕獲socket,並通過recv獲取消息的內容,通信完成後調用closeSocket關閉這個對應accept到的socket,如果不再需要等待任何客戶端連接,那麽用closeSocket關閉掉自身的socket。
  客戶端:建立socket,通過端口號和地址確定目標服務器,使用Connect連接到服務器,send發送消息,等待處理,通信完成後調用closeSocket關閉socket。

1.3.2 編程步驟
  (1)服務端
    加載套接字庫,創建套接字(WSAStartup()/socket());
    綁定套接字到一個IP地址和一個端口上(bind());
    將套接字設置為監聽模式等待連接請求(listen());
    請求到來後,接受連接請求,返回一個新的對應於此次連接的套接字(accept());
    用返回的套接字和客戶端進行通信(send()/recv());
    返回,等待另一個連接請求;
    關閉套接字,關閉加載的套接字庫(closesocket()/WSACleanup());
  (2)客戶端
    加載套接字庫,創建套接字(WSAStartup()/socket());
    向服務器發出連接請求(connect());
    和服務器進行通信(send()/recv());
    關閉套接字,關閉加載的套接字庫(closesocket()/WSACleanup());

1.4 函數詳解

1.4.1 socket()函數
  int socket(int protofamily, int type, int protocol);//返回sockfd

  sockfd是描述符。socket函數對應於普通文件的打開操作。普通文件的打開操作返回一個文件描述字,而socket()用於創建一個socket描述符(socket descriptor),它唯一標識一個socket。這個socket描述字跟文件描述字一樣,後續的操作都有用到它,把它作為參數,通過它來進行一些讀寫操作。
創建socket的時候,可以指定不同的參數創建不同的socket描述符,socket函數的三個參數分別為:
  protofamily:即協議域,又稱為協議族(family)。常用的協議族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。協議族決定了socket的地址類型,在通信中必須采用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。
  type:指定socket類型。常用的socket類型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。
  protocol:故名思意,就是指定協議。常用的協議有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議(這個協議我將會單獨開篇討論!)。
  註意:並不是上面的type和protocol可以隨意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol為0時,會自動選擇type類型對應的默認協議。
  當我們調用socket創建一個socket時,返回的socket描述字它存在於協議族(address family,AF_XXX)空間中,但沒有一個具體的地址。如果想要給它賦值一個地址,就必須調用bind()函數,否則就當調用connect()、listen()時系統會自動隨機分配一個端口。

1.4.2 bind()函數
  正如上面所說bind()函數把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。
  int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  函數的三個參數分別為:
  sockfd:即socket描述字,它是通過socket()函數創建了,唯一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。
  addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址。
  addrlen:對應的是地址的長度。
  通常服務器在啟動的時候都會綁定一個眾所周知的地址(如ip地址+端口號),用於提供服務,客戶就可以通過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是為什麽通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。

1.4.3 listen()、connect()函數
  如果作為一個服務器,在調用socket()、bind()之後就會調用listen()來監聽這個socket,如果客戶端這時調用connect()發出連接請求,服務器端就會接收到這個請求。
  int listen(int sockfd, int backlog);
  int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  listen函數的第一個參數即為要監聽的socket描述字,第二個參數為相應socket可以排隊的最大連接個數。socket()函數創建的socket默認是一個主動類型的,listen函數將socket變為被動類型的,等待客戶的連接請求。
  connect函數的第一個參數即為客戶端的socket描述字,第二參數為服務器的socket地址,第三個參數為socket地址的長度。客戶端通過調用connect函數來建立與TCP服務器的連接。

1.4.4 accept()函數
  TCP服務器端依次調用socket()、bind()、listen()之後,就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()之後就向TCP服務器發送了一個連接請求。TCP服務器監聽到這個請求之後,就會調用accept()函數取接收請求,這樣連接就建立好了。之後就可以開始網絡I/O操作了,即類同於普通文件的讀寫I/O操作。
  int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回連接connect_fd
  參數sockfd
  參數sockfd就是上面解釋中的監聽套接字,這個套接字用來監聽一個端口,當有一個客戶與服務器連接時,它使用這個一個端口號,而此時這個端口號正與這個套接字關聯。當然客戶不知道套接字這些細節,它只知道一個地址和一個端口號。
  參數addr
  這是一個結果參數,它用來接受一個返回值,這返回值指定客戶端的地址,當然這個地址是通過某個地址結構來描述的,用戶應該知道這一個什麽樣的地址結構。如果對客戶的地址不感興趣,那麽可以把這個值設置為NULL。
  參數len

  它也是結果的參數,用來接受上述addr的結構的大小的,它指明addr結構所占有的字節個數。同樣的,它也可以被設置為NULL。
  如果accept成功返回,則服務器與客戶已經正確建立連接了,此時服務器通過accept返回的套接字來完成與客戶的通信。
註意:
accept默認會阻塞進程,直到有一個客戶連接建立後返回,它返回的是一個新可用的套接字,這個套接字是連接套接字。
此時我們需要區分兩種套接字,
監聽套接字: 監聽套接字正如accept的參數sockfd,它是監聽套接字,在調用listen函數之後,是服務器開始調用socket()函數生成的,稱為監聽socket描述字(監聽套接字)
連接套接字:一個套接字會從主動連接的套接字變身為一個監聽套接字;而accept函數返回的是已連接socket描述字(一個連接套接字),它代表著一個網絡已經存在的點點連接。

  一個服務器通常通常僅僅只創建一個監聽socket描述字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創建了一個已連接socket描述字,當服務器完成了對某個客戶的服務,相應的已連接socket描述字就被關閉。
自然要問的是:為什麽要有兩種套接字?原因很簡單,如果使用一個描述字的話,那麽它的功能太多,使得使用很不直觀,同時在內核確實產生了一個這樣的新的描述字。
  連接套接字socketfd_new 並沒有占用新的端口與客戶端通信,依然使用的是與監聽套接字socketfd一樣的端口號

1.4.5 read()、write()等函數
  萬事具備只欠東風,至此服務器與客戶已經建立好連接了。可以調用網絡I/O進行讀寫操作了,即實現了網咯中不同進程之間的通信!網絡I/O操作有下面幾組:
  read()/write();recv()/send();readv()/writev();recvmsg()/sendmsg();recvfrom()/sendto()

  int send( SOCKET s, const char FAR *buf, int len,int flags);
  不論是客戶還是服務器應用程序都用send函數來向TCP連接的另一端發送數據。
  客戶程序一般用send函數向服務器發送請求,而服務器則通常用send函數來向客戶程序發送應答。
  該函數的第一個參數指定發送端套接字描述符;第二個參數指明一個存放應用程序要發送數據的緩沖區;第三個參數指明實際要發送的數據的字節數;第四個參數一般置0。
  int recv( SOCKET s, char FAR *buf, int len,int flags);
  不論是客戶還是服務器應用程序都用recv函數從TCP連接的另一端接收數據。
  該函數的第一個參數指定接收端套接字描述符;第二個參數指明一個緩沖區,該緩沖區用來存放recv函數接收到的數據;第三個參數指明buf的長度;第四個參數一般置0。

1.4.6 close()函數
  在服務器與客戶端建立連接之後,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述字,好比操作完打開的文件要調用fclose關閉打開的文件。
  #include <unistd.h>
  int close(int fd);
  close一個TCP socket的缺省行為時把該socket標記為以關閉,然後立即返回到調用進程。該描述字不能再由調用進程使用,也就是說不能再作為read或write的第一個參數。
  註意:close操作只是使相應socket描述字的引用計數-1,只有當引用計數為0的時候,才會觸發TCP客戶端向服務器發送終止連接請求。

(內容來源於互聯網,如有侵權請及時聯系作者)

Socket (一) 基礎及接口函數