1. 程式人生 > >網路程式設計詳解-含一些開發總結

網路程式設計詳解-含一些開發總結

        開始介紹前,說點經驗之談,希望能有所幫助,在專案開發中肯定涉及到多程序/執行緒,這時使用網路程式設計的系統調應十分小心,也就是在程式設計時應注意:

        (1)假如父程序服務端,子程序客戶端(發個心跳什麼的),若用子程序建立socket,然後利用該socket去發心跳,這樣是行不通的,因為每個子程序socket產生的fd是相同的(多數人會認為創建出的fd不同),所以子程序中bind的是同一個fd,肯定error嘛。最好應該在父程序就建立好多條sokect,在子程序中去使用,提醒:合理使用容器很重要(vector+unordered_map)。

        (2)子程序除了發心跳,肯定要處理什麼event吧,我們在接受事件(accept)時應該小心“驚群效應”,也就是在父程序listen,子程序accept時,會出現多個子程序同時去accept的現象,但我們只希望一個子程序執行accept,所以在應加鎖,子程序accept和recv完後,再釋放鎖。

        (3)epoll_wait也有不少坑需要避讓,詳見:epoll詳解(邊沿觸發/水平觸發),小心epoll_wait

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

       功能:實現同一主機 或 網路中的不同主機 程序間通訊,程序通過返回值訪問套接字。socket預設是阻塞,往往高併發伺服器中需要通過fcntl函式重設定為非阻塞模式。

       返回值:成功,返回套接字的檔案描述符。失敗,返回 -1。

       引數 domain:域,表示通訊特性,可選引數:

        (1)AF_INET:IPv4因特網域。

        (2)AF_INET6:IPv6因特網域。

        (3)AF_UNIX:以後討論,別名:AF_LOCAL。

        (4)AF_UPSPEC:“任何”域。

       引數 type:型別,確定套接字型別,進一步確定通訊特性。可選引數:

        (1)SOCK_DGRAM:固定長度、無連線、不可靠報文傳輸。無連線套接字傳送的報文,每個報文都有目的地址。

        (2)SOCK_RAM:IP協議的資料報介面。

        (3)SOCK_SEQPACKET:固定長度、面向連線、有序、可靠報文傳輸。有連線方式傳送報文,只連線建立好,在訊息傳遞過程中,訊息是不包含目的地址的。

        (4)SOCK_STREAM:有序、可靠、雙向、面向連線的位元組流。

       引數protocol:協議,通常是 0,表示為給定域和型別選擇預設的通訊協議。比如:IPPROTO_IP(ipv4 網際協議)、IPPROTO_IPV6(ipv6 網際協議)。

#include <sys/socket.h>
int shutdown(int sockfd, int how);

        功能:套接字是雙向的,可以使用該函式來禁止一個套接字I/O。

        返回值:成功返回 0;失敗返回 -1。

        引數how:SHUT_RD:關閉讀端。SHUT_WR:關閉寫端。SHUT_RDWR:關閉讀寫端。

        為什麼有了close 還要shutdown 呢?因為close 的關閉只有使用這個套接字的所有引用都關閉才會將套接字關閉,而shutdown 允許套接字處於不活躍狀態, 和引用這個套接字檔案描述符的數目無關。

#include <sys/socket.h>
int bind(int sockfd, const stuct sockaddr *addr, socklen_ len);


struct sockaddr_in {  
    short            sin_family;       // 2 bytes e.g. AF_INET, AF_INET6  
    unsigned short   sin_port;    // 2 bytes e.g. htons(3490)  
    struct in_addr   sin_addr;     // 4 bytes see struct in_addr, below  
    char             sin_zero[8];     // 8 bytes zero this if you want to  
};  

struct in_addr {  
    unsigned long s_addr;          // 4 bytes load with inet_pton(),ip
};  

        功能:將一個客戶端的套接字關聯上一個地址。

        返回值:成功返回 0;失敗返回 -1。

        引數:

        (1)sockfd:socket()函式返回的套接字檔案木描述符。

        (2)sockaddr:sockaddr_in 和 sockaddr都是地址結構,都為16位元組的結構體。核心不關心地址結構(所以之間可進行型別轉換),當它複製或傳遞地址給驅動的時候,它依據 socklen_ 確定需要複製多少資料。

        (3)socklen_:即是sizeof(sockaddr)。

        這個繫結的地址有以下限制:

        (1)這個指定的地址在程序執行的計算機上必須是有效的,不能指定其它機器的地址。

        (2)地址必須和建立套接字時的地址族所支援的格式相匹配。

        (3)地址中的埠不小於1024。

        (4)一般只能將一個套接字端點繫結在一個給定地址上,儘管有些協議支援重繫結。

        可以呼叫getsockname 來發現繫結在套接字上的地址;用getpeername 來找到連線上的對方地址。

        

        由於網路側採用大端模式進行資料的傳輸,可我們的主機不一定都採用大端方式進行資料儲存,比如x86架構小端、c51架構大端(《can通訊》裡詳細說明),所以給sockaddr_in 賦值時往往會用到這幾個函式進行資料的轉換:

    

(1)u_short htons( u_short hostshort ):host to net short,將埠號填入sockaddr_in.sin_port,會使用到該函式進行轉換。

(2)uint32_t htonl(  uint32_t hostlong ):host to net long,將ip填入sockaddr_in.sin_addr.s_addr,會使用到該函式進行轉換。

(3)char * inet_ntoa(struct  in_addr):常用於,accept收到的sockaddr_in.sin_addr轉換為點分十進位制。

(4) in_addr_t inet_addr(const char *):將點分十進位制字串,轉為無符號長整型。使用該函式,也自動將資料轉為了大端。

(5)in_port_t atoi(const char *):字串轉整形。
#include <sys/socket.h>
int connect(int sockfd, const stuct sockaddr *addr, socklen_ len);

        功能:建立連線。。

        返回值:成功返回 0;失敗返回 -1。

        引數:指定地址是我們想通訊的伺服器地址。如果如果sockfd沒有繫結地址,connect會給呼叫者繫結一個預設地址。

#include <sys/socket.h>
int listen(int sockfd, int backlog);

        功能:接受連線請求。listen只是將主動套接字介面(sockfd)變為被動套接字介面,設定核心接受此套接字連線請求。並不建立連線,三次握手發生在listen和accept之間

        返回值:成功返回 0;失敗返回 -1。

        引數:backlog,表示已完成3次握手,但未accept的最多連線數,這個值的指定只是參考,實際上核心會做一些調整。一旦該佇列滿,系統便會拒絕多餘連線請求。

        細節:對於上述的“三次握手連線是在listen和accept之間完成的”這句話進行解釋,listen將“主動套接字介面”置為“被動套接字介面”後,該介面就可以稱之為監聽介面,核心為監聽埠維護兩個佇列:(1)未完成連線的佇列(伺服器處於SYN_RECV狀態)。(2)完成連線的佇列。accept是從完成連線佇列中取出最前邊使用,並將其改一個新fd。

        3次握手的步驟:

        (1)客戶端close,服務端listen。

        (2)客戶端傳送SYN,處於SYN_SEND狀態。(一次)

        (3)服務端收到該資訊,變為SYN_RECV狀態,並回復客戶端。(二次)

        (4)客戶端收到訊息,變為ESTABLISHED狀態,併發ACK確認訊息。(三次)

        (5)服務端收到到ACK,變為ESTABLISHED狀態。

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *len);

        功能:獲得連線請求,返回新的套接字fd。

        返回值:成功,返回套接字的檔案描述符。失敗,返回 -1。

        引數:引數 sockfd:表示原始套接字檔案描述符,與返回的檔案描述符是不同的。參數 addr:用來存放客戶端的地址資訊,不使用置為NULL。引數 len:表示客戶端地址的大小,不使用置為NULL

        細節:為什麼accept要放在三次握手後呢?其實在以前accept是放在上述三次握手的第三步,accept會分配資源給連線,現假設在第三步後accept,此時連線還沒有建立,要是第四步客戶端不進行回覆,那就是既沒建立好連線卻又分配了資源,這個漏洞導致了“DDOS攻擊”的產生。

#include <sys/socket.h>
int send( SOCKET s, const char FAR *buf, int len, int flags );

        功能:從tcp連線到一段將訊息發往另一端。

        返回值:成功返回位元組數,失敗返回 SOCKET_ERROR,即-1。

        引數:

        (1)第一個引數是需要使用的套接字連線符,accept後改變的那個。

        (2)要傳送的資料存放位置。

        (3)資料大小,位元組。

        (4)通常為0。

#include <sys/socket.h>
int recv( SOCKET s, char FAR *buf, int len, int flags);

        和 send 函式基本相同,只要注意與send函式的buf 的區別就好。

int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);

        功能:設定某level 的狀態。

        返回值:成功0。失敗-1,errno被設定。

        引數:細明參見:https://blog.csdn.net/xioahw/article/details/4056514

        這裡說明一下level為:SOL_SOCKET ,optname為:SO_REUSEADDR,這時表示埠可重用,當optval非0時,重用bind中的地址。

        該函式有關心跳包的設定:https://blog.csdn.net/aa2650/article/details/17027845https://blog.csdn.net/ctthuangcheng/article/details/8596818

 

        網路程式設計注意點:

        (1) 多個子程序socket產生的socket_fd數值是一樣的,但表示的是不同的套接字,因為fd的使用範圍是程序,不同程序,相同的fd,也可能表示不同檔案。

        (2) 正常情況下,多程序不能同時bind同一個ip:port,如果有這樣的需求,可使用setsockopt設定SO_REUSEPORT。SO_REUSEPORT是避免“驚群”的最好方法了,暫時沒有之一

        (3) 服務端多個程序使用SO_REUSEPORT,bind同一ip:port,accept時不會報錯的。但是客戶端多個程序使用SO_REUSEPORT,bind同一ip:port,在connect時還是會報錯的。