socket編程(C++)


介紹

​ 網絡上的兩個程序通過一個雙向的通信連接實現數據的交換,這個連接的一端稱為一個socket。

過程介紹

​ 服務器端和客戶端通信過程如下所示:

![socket通信過程](http://images.cnblogs.com/cnblogs_com/helloworldcode/1414395/o_05232335-fb19fc7527e944d4845ef40831da4ec2.png)

服務端

​ 服務端的過程主要在該圖的左側部分,下面對上圖的每一步進行詳細的介紹。

1. 套接字對象的創建

	/*
     * _domain 套接字使用的協議族信息
     * _type 套接字的傳輸類型
     * __protocol 通信協議
     * */
     int socket (int __domain, int __type, int __protocol) __THROW;

socket起源於UNIX,在Unix一切皆文件哲學的思想下,socket是一種"打開—讀/寫—關閉"模式的實現,可以將該函數類比常用的open()函數,服務器和客戶端各自維護一個"文件",在建立連接打開后,可以向自己文件寫入內容供對方讀取或者讀取對方內容,通訊結束時關閉文件。

參數介紹

第一個參數:關於協議族信息可選字段如下,只列出一般常見的字段。

地址族 含義
AF_INET IPv4網絡協議中采用的地址族
AF_INET6 IPv6網絡協議中采用的地址族
AF_LOCAL 本地通信中采用的UNIX協議的地址族(用的少)

第二個參數:套接字類型。常用的有SOCKET_RAW,SOCK_STREAM和SOCK_DGRAM。

套接字類型 含義
SOCKET_RAW 原始套接字(SOCKET_RAW)允許對較低層次的協議直接訪問,比如IP、 ICMP協議。
SOCK_STREAM SOCK_STREAM是數據流,一般為TCP/IP協議的編程。
SOCK_DGRAM SOCK_DGRAM是數據報,一般為UDP協議的網絡編程;

第三個參數:最終采用的協議。常見的協議有IPPROTO_TCP、IPPTOTO_UDP。如果第二個參數選擇了SOCK_STREAM,那么采用的協議就只能是IPPROTO_TCP;如果第二個參數選擇的是SOCK_DGRAM,則采用的協議就只能是IPPTOTO_UDP。

2. 向套接字分配網絡地址——bind()

/* 
* __fd:socket描述字,也就是socket引用
* myaddr:要綁定給sockfd的協議地址
* __len:地址的長度
*/
int bind (int __fd, const struct sockaddr* myaddr, socklen_t __len)  __THROW;

第一個參數:socket文件描述符__fd即套接字創建時返回的對象,

第二個參數:myaddr則是填充了一些網絡地址信息,包含通信所需要的相關信息,其結構體具體如下:

struct sockaddr
  {
    sa_family_t sin_family;	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };

在具體傳參的時候,會用該結構體的變體sockaddr_in形式去初始化相關字段,該結構體具體形式如下,結構體sockaddr中的sa_data就保存着地址信息需要的IP地址和端口號,對應着結構體sockaddr_insin_portsin_addr字段。

struct sockaddr_in{
    sa_family_t sin_family;		//前面介紹的地址族
    uint16_t sin_port;			//16位的TCP/UDP端口號
    struct in_addr sin_addr;	//32位的IP地址
    char sin_zero[8];			//不使用
}

in_addr 結構定義如下:

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{
	in_addr_t s_addr;
};

sin_zero 無特殊的含義,只是為了與下面介紹的sockaddr結構體一致而插入的成員。因為在給套接字分配網絡地址的時候會調用bind函數,其中的參數會把sockaddr_in轉化為sockaddr的形式,如下:

struct sockaddr_in serv_addr;
...
bind(serv_socket, (struct sockaddr*)&serv_addr, sizeof(serv_addr);

需要注意的是s_addr是一種uint32_t類型的數據,而且在網絡傳輸時,統一都是以大端序的網絡字節序方式傳輸數據,而我們通常習慣的IP地址格式是點分十進制,例如:“219.228.148.169”,這個時候就會調用以下函數進行轉化,將IP地址轉化為32位的整數形數據,同時進行網絡字節轉換:

in_addr_t inet_addr (const char *__cp) __THROW;
//或者
int inet_aton (const char *__cp, struct in_addr *__inp) __THROW;	//windows無此函數

如果單純要進行網絡字節序地址的轉換,可以采用如下函數:

/*Functions to convert between host and network byte order.

   Please note that these functions normally take `unsigned long int' or
   `unsigned short int' values as arguments and also return them.  But
   this was a short-sighted decision since on different systems the types
   may have different representations but the values are always the same.  */

// h代表主機字節序
// n代表網絡字節序
// s代表short(4字節)
// l代表long(8字節)
extern uint32_t ntohl (uint32_t __netlong) __THROW __attribute__ ((__const__));
extern uint16_t ntohs (uint16_t __netshort)
     __THROW __attribute__ ((__const__));
extern uint32_t htonl (uint32_t __hostlong)
     __THROW __attribute__ ((__const__));
extern uint16_t htons (uint16_t __hostshort)

3. 進入等待連接請求狀態

給套接字分配了所需的信息后,就可以調用listen()函數對來自客戶端的連接請求進行監聽(客戶端此時要調用connect()函數進行連接)

/* Prepare to accept connections on socket FD.
   N connection requests will be queued before further requests are refused.
   Returns 0 on success, -1 for errors.  */
extern int listen (int __fd, int __n) __THROW;

第一個參數:socket文件描述符__fd,分配所需的信息后的套接字。

第二個參數:連接請求的隊列長度,如果為6,表示隊列中最多同時有6個連接請求。

這個函數的fd(socket套接字對象)就相當於一個門衛,對連接請求做處理,決定是否把連接請求放入到server端維護的一個隊列中去。

4. 受理客戶端的連接請求

listen()中的sock(__fd : socket對象)發揮了服務器端接受請求的門衛作用,此時為了按序受理請求,給客戶端做相應的回饋,連接到發起請求的客戶端,此時就需要再次創建另一個套接字,該套接字可以用以下函數創建:

/* Await a connection on socket FD.
   When a connection arrives, open a new socket to communicate with it,
   set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
   peer and *ADDR_LEN to the address's actual length, and return the
   new socket's descriptor, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int accept (int __fd, struct sockaddr *addr, socklen_t *addr_len);

函數成功執行時返回socket文件描述符,失敗時返回-1。

第一個參數:socket文件描述符__fd,要注意的是這個套接字文件描述符與前面幾步的套接字文件描述符不同。

第二個參數:保存發起連接的客戶端的地址信息。

第三個參數: 保存該結構體的長度。

5. send/write發送信息

linux下的發送函數為:

/* Write N bytes of BUF to FD.  Return the number written, or -1.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
 ssize_t write (int __fd, const void *__buf, size_t __n) ;

而在windows下的發送函數為:

ssize_t send (int sockfd, const void *buf, size_t nbytes, int flag) ;

第四個參數是傳輸數據時可指定的信息,一般設置為0。

6. recv/read接受信息

linux下的接收函數為

/* Read NBYTES into BUF from FD.  Return the
   number read, -1 for errors or 0 for EOF.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
ssize_t read (int __fd, void *__buf, size_t __nbytes);

而在windows下的接收函數為

ssize_t recv(int sockfd, void *buf, size_t nbytes, int flag) ;

7. 關閉連接

/* Close the file descriptor FD.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
int close (int __fd);

退出連接,此時要注意的是:調用close()函數即表示向對方發送了EOF結束標志信息


客戶端

​ 服務端的socket套接字在綁定自身的IP即 及端口號后這些信息后,就開始監聽端口等待客戶端的連接請求,此時客戶端在創建套接字后就可以按照如下步驟與server端通信,創建套接字的過程不再重復了。

1. 請求連接

/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
   For connectionless socket types, just set the default address to send to
   and the only address from which to accept transmissions.
   Return 0 on success, -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
int connect (int socket, struct sockaddr* servaddr, socklen_t addrlen);

幾個參數的意義和前面的accept函數意義一樣。要注意的是服務器端收到連接請求的時候並不是馬上調用accept()函數,而是把它放入到請求信息的等待隊列中。

套接字的多種可選項

可以通過如下函數對套接字可選項的參數進行獲取以及設置。

/* Put the current value for socket FD's option OPTNAME at protocol level LEVEL
   into OPTVAL (which is *OPTLEN bytes long), and set *OPTLEN to the value's
   actual length.  Returns 0 on success, -1 for errors.  */
extern int getsockopt (int sock, int __level, int __optname,
		       void *__optval, socklen_t *optlen) __THROW;

/* Set socket FD's option OPTNAME at protocol level LEVEL
   to *OPTVAL (which is OPTLEN bytes long).
   Returns 0 on success, -1 for errors.  */
extern int setsockopt (int sock, int __level, int __optname,
		       const void *__optval, socklen_t __optlen) __THROW;

參數說明:

scok: 套接字的文件描述符

** __level ** :可選項的協議層,如下:

協議層 功能
SOL_SOCKET 套接字相關通用可選項的設置
IPPROTO_IP 在IP層設置套接字的相關屬性
IPPROTO_TCP 在TCP層設置套接字相關屬性

** __optname ** :要查看的可選項名,幾個主要的選項如下

選項名 說明 數據類型 所屬協議層
SO_RCVBUF 接收緩沖區大小 int SOL_SOCKET
SO_SNDBUF 發送緩沖區大小 int SOL_SOCKET
SO_RCVLOWAT 接收緩沖區下限 int SOL_SOCKET
SO_SNDLOWAT 發送緩沖區下限 int SOL_SOCKET
SO_TYPE 獲得套接字類型(這個只能獲取,不能設置) int SOL_SOCKET
SO_REUSEADDR 是否啟用地址再分配,主要原理是操作關閉套接字的Time-wait時間等待的開啟和關閉 int SOL_SOCKET
IP_HDRINCL 在數據包中包含IP首部 int IPPROTO_IP
IP_MULTICAST_TTL 生存時間(Time To Live),組播傳送距離 int IPPROTO_IP
IP_ADD_MEMBERSHIP 加入組播 int IPPROTO_IP
IP_OPTINOS IP首部選項 int IPPROTO_IP
TCP_NODELAY 不使用Nagle算法 int IPPROTO_TCP
TCP_KEEPALIVE TCP保活機制開啟下,設置保活包空閑發送時間間隔 int IPPROTO_TCP
TCP_KEEPINTVL TCP保活機制開啟下,設置保活包無響應情況下重發時間間隔 int IPPROTO_TCP
TCP_KEEPCNT TCP保活機制開啟下,設置保活包無響應情況下重復發送次數 int IPPROTO_TCP
TCP_MAXSEG TCP最大數據段的大小 int IPPROTO_TCP

** __optval ** :保存查看(get)/更改(set)的結果

** optlen ** : 傳遞第四個參數的字節大小

這里只對幾個可選項參數進行說明:

1.設置可選項的IO緩沖區大小

參考案例如下:

int status, snd_buf;
socklen_t len = sizeof(snd_buf);
status = getsockopt(serv_socket, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
cout << "發送緩沖區大小: " << snd_buf <<endl;

雖然可以獲得的接收/發送緩沖區的大小,但是通過設置接收/發送緩沖區大小時,得到的結果會與我們期望的不一樣,因為對緩沖區大小的設置是一件很謹慎的事,其自身會根據設置的值進行一定的優化。

2. 是否啟用地址再分配與Time-wait時間等待

關於地址再分配問題會發生在這種情況下,首先看兩種情況,假設客戶端和服務器正在通訊(測試代碼下載地址)。

① 在客戶端的終端按Crtl + c或者其他方式斷開與服務器的連接,此時客戶端發送FIN消息,經過四次握手斷開連接,操作系統關閉套接字,相當於close()的過程。然后在次啟動客戶端,順利啟動。

② 在服務端的終端按Crtl + c或者其他方式斷開與客戶端的連接,像①中一樣,再次啟動服務端,此時出現bind() error錯誤。

服務器端出現這種情況的原因是調用套接字分配網絡地址函數bind()的時候之前使用建立連接的同一端口號還沒有來得及停用(大約要過兩三分鍾才處於可用狀態),而客戶端申請連接的端口是任意指定,程序運行時會動態分配端口號。

服務器端端口沒有被釋放到被釋放的時間狀態稱為Time-wait狀態,這個狀態的出現可以借助TCP斷開連接的四次握手協議來分析,如下圖:

當client端發送ACK=1 ack=k+1這個消息給服務端就立即消除套接字,若此時該消息中途傳輸被遺失,則這個時候server端就永遠無法收到client的ACK消息了。

3. TCP_NODELAY

TCP套接字默認是使用Nagle算法的,該算法的特征是只有收到前一條數據的ACK消息后,才會發送下一條數據。

從網上找到一張圖說明使用和禁用Nagle算法的區別(圖片來源),如下:

設置代碼如下:

#include <netinet/tcp.h> //注意要引入這個頭文件

int opt_val = 1;
setsockopt(serv_socket, IPPROTO_TCP, TCP_NODELAY, (void*)&opt_val, sizeof(opt_val));

程序案例

案例的過程,在網上看到了關於read和write的發送與接受過程的圖,便於理解:

![](http://images.cnblogs.com/cnblogs_com/helloworldcode/1414395/o_TCP-socket.jpg)

代碼鏈接 github

注意以上代碼都是在ubuntu下運行的,在windows的代碼與此有所不同。比如要引入一個<winsock2.h>的頭文件,調用WSAStartup(...)函數進行Winsock的初始化,而且它們的接受與發送函數也有所不同。

在我的github上有幾個簡單的demo,可供學習

參考文獻

簡單理解Socket

套接字

《TCP/IP網絡編程》尹聖雨


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM