unix網(wǎng)絡(luò)編程

套接字地址結(jié)構(gòu)

ipv4套接字地址結(jié)構(gòu)

POSIX定義如下:

struct in_addr {
  in_addr_t s_addr; /* 32bit ipv4 address */
                                    /* network byte ordered */
}

struct sockaddr_in {
  uint8_t                   sin_len;        /* length of structure */
  sa_family_t           sin_family; /* AF_INET */
  in_port_t             sin_port;       /* 16-bit TCP or UDP port number */
                                                        /* network byte ordered */
  struct in_addr    sin_addr;       /* 32-bit ipv4 address */
                                                        /* network byte ordered */
  char                      sin_zero[8];/* unused */
}
  • sin_len字段,是由處理來自不同協(xié)議族的套接字地址結(jié)構(gòu)的例程(例如路由表處理代碼)在內(nèi)核中使用的啤咽,無須設(shè)置和檢查它;

  • sin_family sa_port sa_addr三個字段是posix規(guī)范的渠脉,其他字段也是可以接受的宇整,sa_port 與sa_addr要求是網(wǎng)絡(luò)字節(jié)序

  • ipv4地址使用:存在兩種不同訪問方法芋膘,1)serv.sin_addr其類型為in_addr結(jié)構(gòu)體鳞青;2)serv.sin_addr.s_addr其類型為in_addr_t(通常為無符號32位整數(shù));

    歷史原因struct in_addr結(jié)構(gòu)中只存在一個成員为朋。因之前作為聯(lián)合使用臂拓,后面廢棄;

通用套接字地址結(jié)構(gòu)

<sys/socket.h>頭文件定義如下:

struct sockaddr {
  uint8_t           sa_len;         
  sa_family_t   sa_family;      /* address family: AF_xxx value */
  char              sa_data[14];    /* protocol-specific address */
}

其中bind()函數(shù)入?yún)⑹褂玫木褪窃摻Y(jié)構(gòu)定義:

int bind(int, struct sockaddr *, socklen_t);

因此习寸,使用ipv4套接字地址結(jié)構(gòu)需要強制類型轉(zhuǎn)換胶惰;

主機字節(jié)序

內(nèi)存存儲多字節(jié)數(shù)據(jù)由于不同系統(tǒng)存儲方式不同,將某個給定系統(tǒng)所用的字節(jié)序叫做主機字節(jié)序霞溪,分為兩種模式:

  • 小端字節(jié)序

    低序字節(jié)在低地址孵滞,高序字節(jié)在高地址中捆;

  • 大端字節(jié)序

    與小端相反,低序字節(jié)在高地址坊饶,高序字節(jié)在低地址泄伪;


    image.png

網(wǎng)絡(luò)字節(jié)序

網(wǎng)絡(luò)協(xié)議必須指定一個網(wǎng)絡(luò)字節(jié)序,網(wǎng)際協(xié)議使用大端字節(jié)序來傳送多字節(jié)整數(shù)(如16位端口及32位的ipv4地址)幼东,并且套接字地址結(jié)構(gòu)中的端口及地址必須按照網(wǎng)絡(luò)字節(jié)序來維護臂容,因此,需要關(guān)注如何在主機字節(jié)序和網(wǎng)絡(luò)字節(jié)序之間的轉(zhuǎn)換根蟹,<netinet/in.h>提供了轉(zhuǎn)換函數(shù):

uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);
uint16_t ntohs(uint16_t net16bitvalue);
uint32_t ntohl(uint32_t net32bitvalue);

h代表主機字節(jié)序脓杉,n代表網(wǎng)絡(luò)字節(jié)序,s代表short简逮,l代表long球散;

地址轉(zhuǎn)換函數(shù)

將ASCII字符串(偏愛使用的格式)與網(wǎng)絡(luò)字節(jié)序的二進制值 之間轉(zhuǎn)換網(wǎng)際地址;

#include <arpa/inet.h>
//適用ipv4
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
char *inet_ntoa(struct in_addr inaddr);

//適用ipv4 ipv6
int inet_pton(int family, const char *strptr, void *addrptr);
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
//family可以為AF_INET或者為AF_IENT6

其中inet_addr已廢棄散庶,使用inet_aton蕉堰;

基本套接字編程

socket()

套接字描述符如文件描述符一樣,存在引用計數(shù)悲龟,如fork被子進程復(fù)制后引用計數(shù)+1屋讶,只有引用計數(shù)為0時,內(nèi)核才會關(guān)閉該描述符须教;

connect()

connect函數(shù)前為啥不需要bind綁定地址皿渗,因為內(nèi)核會根據(jù)外出網(wǎng)絡(luò)接口確定源ip地址(而所用網(wǎng)絡(luò)接口則取決于到達服務(wù)器所需的路徑),并選擇一個臨時端口作為源端口轻腺;

connect會激發(fā)tcp的三次握手乐疆,具體出錯情況如下:

  1. ETIMEOUT超時錯誤

    tcp發(fā)出SYN分節(jié)但未響應(yīng),超時繼續(xù)發(fā)送直至達到一定的超時時間就會出現(xiàn)此錯誤贬养;

  2. ECONNREFUSED連接拒絕錯誤

    tcp收到的SYN響應(yīng)為RST復(fù)位數(shù)據(jù)包挤土,則表明服務(wù)端指定連接的端口沒有進程在等待與之連接;

    產(chǎn)生`RST`數(shù)據(jù)包的三個條件:
    1. 目的地端口的SYN數(shù)據(jù)包達到后误算,該端口上沒有正在監(jiān)聽的服務(wù)器(如前所述)仰美;
    2. tcp取消已有的一個連接;
    3. tcp收到一個根本不存在的連接上的數(shù)據(jù)包儿礼;
    
  3. EHOSTUNREACH 或 ENETUNREACH未達到錯誤

    image.png

    若connect失敗筒占,則tcp由SYN_SENT轉(zhuǎn)為CLOSED關(guān)閉狀態(tài),此時sokcet已不再可用蜘犁,必須關(guān)閉重新調(diào)用socket創(chuàng)建;

bind()

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

不管對于客戶端還是服務(wù)端止邮,bind函數(shù)都可以不需要这橙;對于客戶端connect 如上面所述奏窑;對于服務(wù)端,內(nèi)核會根據(jù)客戶端發(fā)送的SYN數(shù)據(jù)包(段)的目的地址作為服務(wù)端的源IP地址屈扎,而對于端口埃唯,若不指定,則客戶端無法獲取指定端口鹰晨,但也可以通過getsockname來獲取相應(yīng)的端口墨叛;

對于客戶端其實也可以使用bind函數(shù)綁定地址及端口,但一般這么使用模蜡;

bind綁定的地址必須為所在主機的網(wǎng)絡(luò)接口之一

調(diào)用bind可以指定ip地址或端口漠趁,也可以不指定,具體如下:


image.png

bind函數(shù)返回錯誤:

  • EADDRINUSE("address already in usr"忍疾,地址已在使用)闯传;

listen()

非阻塞接口,主要指定內(nèi)核兩個隊列的最大數(shù)卤妒;

#include <sys/socket.h>

//baklog為內(nèi)核為相應(yīng)的套接字排隊的最大連接個數(shù)甥绿,該值未有一個明確的界限(不能超過資源限制)
int listen(int sockfd, int backlog); //若成功返回0;出錯返回-1

根據(jù)tcp狀態(tài)轉(zhuǎn)移圖,listen后tcp由CLOSED變?yōu)?code>LISTEN狀態(tài)则披;

為了理解backlog參數(shù)共缕,我們必須認識到內(nèi)核為任何一個給定的監(jiān)聽套接字維護兩個隊列:

  • 未完成連接隊列,每個SYN數(shù)據(jù)段對應(yīng)其中的一項

    客戶端發(fā)出SYN數(shù)據(jù)段并到達服務(wù)端士复,這些套接字處于SYN_RCVD狀態(tài)图谷,服務(wù)端正在忙于完成相應(yīng)的三次握手;

  • 已完成連接隊列判没,每個已完成的tcp連接三次握手都對應(yīng)其中的一項蜓萄,這些套接字處于ESTABLISHED

    該隊列會用于accept系統(tǒng)調(diào)用澄峰,如果該隊列為空嫉沽,則進程會睡眠;若不為空俏竞,則會返回隊列的隊首項給進程绸硕;

    image.png

    若兩個隊列已滿,則tcp會忽略SYN數(shù)據(jù)段魂毁,即不會發(fā)送RST數(shù)據(jù)段玻佩;因為客戶端未收到SYN響應(yīng)會重發(fā),直至隊列不滿席楚,且若發(fā)送RST咬崔,會導(dǎo)致客戶端connect錯誤,進而可能導(dǎo)致客戶端退出

accept()

用于從已完成連接隊列隊頭返回下一個已完成連接,如果隊列為空垮斯,則睡眠等待(若套接字為阻塞方式

#include <sys/socket.h>

//參數(shù):sockfd為監(jiān)聽套接字郎仆,cliaddr為返回的客戶端地址,addrlen為客戶端協(xié)議地址大小(若不需要知道客戶端地址及大小兜蠕,則可置為NULL)
//返回值:新的已連接套接字
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

輸入的為監(jiān)聽套接字扰肌,返回的為已連接套接字,區(qū)分兩個套接字熊杨,是保持監(jiān)聽套接字生命周期一直存在(避免關(guān)閉無法獲取新的連接)曙旭,而已連接套接字完成服務(wù)即可關(guān)閉;

close()

#include <unistd.h>

int close(int sockfd);

關(guān)閉套接字描述符晶府,但具體應(yīng)是標記該套接字為已關(guān)閉桂躏,然后立即返回到調(diào)用進程,該套接字不能再被調(diào)用進程使用郊霎,即不能再read() write()操作沼头;然后tcp將嘗試發(fā)送已排隊等候發(fā)送到對端的任何數(shù)據(jù),發(fā)送完畢后正常的tcp連接終止序列操作书劝;若對端已關(guān)閉进倍,是不是應(yīng)該丟棄緩存區(qū)的數(shù)據(jù),終止tcp购对?猾昆?

shutdown()

#include <sys/socket.h>

int shutdown(int sockfd, int howto); //若成功返回0,失敗返回-1

close關(guān)閉套接字存在引用計數(shù)問題骡苞,需要引用計數(shù)為0時才會標記套接字為關(guān)閉垂蜗,內(nèi)核嘗試發(fā)送存在的已排隊等候的任何數(shù)據(jù),且close會關(guān)閉tcp套接字兩個方向的數(shù)據(jù)傳送解幽;

若需要關(guān)閉一方的連接贴见,且不需要顧慮引用計數(shù)問題,可使用shutdown來避免躲株;

具體的howto的值如下:

  • SHUT_RD:關(guān)閉連接的讀這一半

    套接字中不再有數(shù)據(jù)可接收片部,并且套接字接收緩存區(qū)的現(xiàn)有數(shù)據(jù)都被丟棄,進程不能對套接字調(diào)用任何讀函數(shù)霜定;對于tcp套接字調(diào)用shutdown后档悠,由該套接字接收的對端任何數(shù)據(jù)都被確認,然后悄然丟棄望浩;

  • SHUT_WR:關(guān)閉連接的寫這一半

    對于tcp套接字稱為半關(guān)閉辖所,當前留在發(fā)送緩存區(qū)的數(shù)據(jù)將被發(fā)送掉,后跟tcp的正常連接終止序列磨德;進程不能再對該套接字調(diào)用任何寫函數(shù)缘回;且不受引用計數(shù)影響;

  • SHUT_RDWR:讀寫連接都關(guān)閉,相當于調(diào)用兩次shutdown:第一次調(diào)用SHUT_RD切诀,第二次調(diào)用SHUT_WR揩环;

getsockname() getpeername()

#include <sys/socket.h>

int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);

getsockname用于獲取綁定的sockfd套接字描述符的本地地址,主要場景如下:

  • bind綁定本地地址及端口或者綁定通配地址或0端口幅虑,獲取由內(nèi)核分配的本地地址及本地端口;
  • 使用bind綁定本地地址及為0端口(由內(nèi)核去選擇本地端口)后顾犹,獲取由內(nèi)核賦予的本地端口倒庵;
  • 使用bind綁定通配地址及本地端口,獲取由內(nèi)核分配的本地地址炫刷;
  • 獲取某個套接字的地址族擎宝;

getpeername用于獲取綁定的sockfd套接字描述符的外地地址,主要場景如下:

  • 服務(wù)端fork子進程去處理tcp連接后浑玛,又執(zhí)行exec新的程序绍申,導(dǎo)致新的程序無法獲取accept返回的外地地址,但共享連接套接字的特性顾彰,連接套接字不會丟失极阅,通過該套接字獲取外地地址;

值-結(jié)果

對于socket編程接口涨享,如accept recvfrom getsockname getpeername等函數(shù)來獲取來源地址或者綁定套接字的地址筋搏,其中傳入len長度類似為socklent_t,且傳入的是指針類型厕隧,接口調(diào)用時該值用于告知內(nèi)核addr長度奔脐,返回結(jié)果是,內(nèi)核修改addr返回具體的地址吁讨,因此髓迎,調(diào)用這些接口是需要傳入struct sockaddr長度

套接字編程異常處理

信號

1. SIGCHLD

對于服務(wù)端fork子進程并發(fā)處理請求建丧,若listen監(jiān)聽主進程不處理已結(jié)束的子進程排龄,將會導(dǎo)致子進程稱為僵死進程,若存在大量這樣進程將占用系統(tǒng)資源導(dǎo)致系統(tǒng)異常茶鹃;

處理:子進程退出時會向主進程發(fā)送SIGCHLD信號涣雕,父進程應(yīng)捕獲wait處理;若同時存在大量僵死進程闭翩,wait只會處理第一個停止的子進程挣郭,需要循環(huán)使用waitpid來處理子進程狀態(tài)并回收系統(tǒng)資源;

2. SIGPIPE

對于服務(wù)端進程崩潰或異常終止情況疗韵,服務(wù)端進程退出會關(guān)閉套接字描述符兑障,進而會發(fā)送FIN數(shù)據(jù)段至客戶主機,服務(wù)端tcp狀態(tài)處于FIN_WAIT_2,客戶端tcp狀態(tài)處于CLOSE_WAIT流译;若此時客戶端繼續(xù)write發(fā)送數(shù)據(jù)逞怨,則對于第一次發(fā)送,會收到服務(wù)端主機的RST數(shù)據(jù)段福澡;若再次write系統(tǒng)調(diào)用發(fā)送數(shù)據(jù)叠赦,則內(nèi)核會向客戶進程發(fā)送SIGPIPE信號;

對于SIGPIPE信號革砸,客戶端應(yīng)忽略處理除秀,否則默認處理該信號為終止進程;

3. 被中斷的系統(tǒng)調(diào)用

慢系統(tǒng)調(diào)用算利,如阻塞的網(wǎng)絡(luò)系統(tǒng)調(diào)用:read write accept等册踩,阻塞期間被信號中斷,如SIGCHLD信號效拭,就會返回EINTR錯誤暂吉;

需要對中斷處理錯誤繼續(xù)重復(fù)系統(tǒng)調(diào)用;

accept()調(diào)用錯誤

image.png

客戶端與服務(wù)端三次握手建立連接后缎患,客戶主機向服務(wù)端發(fā)送RST數(shù)據(jù)段慕的,這時服務(wù)端再調(diào)用accpet就會返回ECONNECABORTED錯誤(具體看linux內(nèi)核實現(xiàn));

該錯誤為非致命錯誤较锡,因此重復(fù)調(diào)動accpet即可业稼;

(已連接)服務(wù)主機崩潰、崩潰后重啟蚂蕴、關(guān)機

服務(wù)主機崩潰或者網(wǎng)絡(luò)不可達或者崩潰后重啟低散,都會導(dǎo)致:

  • 如果a進程阻塞在read上,那么結(jié)果只能是永遠的等待骡楼。

  • 如果a進程先write然后阻塞在read熔号,由于收不到B機器TCP/IP棧的ack,TCP會持續(xù)重傳12次(時間跨度大約為9分鐘)鸟整,然后在阻塞的read調(diào)用上返回錯誤:ETIMEDOUT/EHOSTUNREACH/ENETUNREACH

  • 假如B機器恰好在某個時候恢復(fù)和A機器的通路引镊,并收到a某個重傳的pack,因為不能識別所以會返回一個RST篮条,此時a進程上阻塞的read調(diào)用會返回錯誤ECONNREST

對于關(guān)機弟头,服務(wù)端進程結(jié)束后會發(fā)送FIN數(shù)據(jù)段;

對于以上情況涉茧,客戶進程無法有效及時感知到服務(wù)端異常情況

處理:tcp keepalive或者應(yīng)用層心跳

數(shù)據(jù)格式

對于應(yīng)用數(shù)據(jù)存在系統(tǒng)內(nèi)存存儲大小端字節(jié)序問題赴恨,如果雙方字節(jié)序不同,會造成數(shù)據(jù)處理異常伴栓;

以相同字符集及字節(jié)序處理伦连;

I/O復(fù)用

對于unix系統(tǒng)I/O模式如下:

  • 阻塞I/O
    如read recvfrom等

  • 非阻塞I/O
    指定recvfrom模式為非阻塞雨饺,調(diào)用返回EWOULDBLOCK錯誤;

  • I/O復(fù)用(如select poll)

  • 信號驅(qū)動I/O(SIGIO)


    image.png
  • 異步I/O(POSIX的aio_系列函數(shù))


    image.png

select()


#include <sys/select.h>
#include <sys/time.h>

struct timeval {
  long tv_sec;//秒
  long tv_usec;//微妙
}

//若有就緒描述符就返回其數(shù)目惑淳,若超時返回0昌屉,若失敗返回-1
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *excepset, 
                        const struct timeval *timeout);

select若有就緒描述符就會set相應(yīng)的位鳖悠,若再次select操作,需要重置fd_set相應(yīng)的位议忽;

  • timeout參數(shù)

    類型為const疙渣,因此不會被修改分尸,若需要測量select時間需要獲取前后時間枪向;

    • 若timeout為0谷羞,不阻塞;
    • 若為NULL痕貌,則一直阻塞;
    • 若不為0糠排,則為超時時間舵稠;
  • maxfd

    為最大描述符值+1,注意不是最大描述符數(shù)目入宦,此值用于內(nèi)核檢索描述符的范圍區(qū)間哺徊;

  • fd_set讀、寫乾闰、異常描述符集

    使用FD_SET FD_CLR FD_ISSET FD_ZERO 這些宏函數(shù)來設(shè)置或測試描述符集落追;

就緒條件

image.png

其中有數(shù)據(jù)可讀或者可寫,指接收緩存區(qū)或者發(fā)送緩存區(qū)數(shù)據(jù)字節(jié)數(shù)大于等于套接字緩存區(qū)低水位標記的大小涯肩,該標記可通過SO_RCVLOWATSO_SNDLOWAT選項修改轿钠;

連接關(guān)閉,指若為讀關(guān)閉(接收了FIN的tcp連接)病苗,則讀不會阻塞且返回0(即返回EOF);若為寫關(guān)閉疗垛,則再寫操作就會產(chǎn)生SIGPIPE信號;

注意若不是套接字描述符硫朦,如標準輸入輸出贷腕,則可讀即內(nèi)核緩存區(qū)存在數(shù)據(jù)可讀,select不會感知用戶緩存區(qū)的大小咬展,因此泽裳,混合使用stdio與select需要小心,可參考select函數(shù)與stdio混用的不良后果

適用場景

  1. 多描述符情況下破婆,可通過select判定哪些描述符可讀寫或異常涮总,并且使用select不會存在無法感知對端異常崩潰或者連接關(guān)閉等情況(如使用其他阻塞調(diào)用未讀寫套接字,就會導(dǎo)致阻塞調(diào)用阻止獲取對端異常情況)荠割;

  2. 使用select替換掉fork子進程來單進程accept多連接妹卿;

    select監(jiān)聽套接字是否可讀旺矾,可判定是否有新的連接請求,再調(diào)用accept獲取已連接套接字夺克,并select監(jiān)聽所有套接字來讀寫數(shù)據(jù)箕宙;

pselect()


#include <sys/select.h>
#include <signal.h>
#include <time.h>

struct timespec {
  long tv_sec;  //秒
  long tv_nsec; //納秒
}

int pselect(int maxfd, fd_set *readset, fd_set *writeset, fd_set *excepset,
            const struct timespec *timeout, const sigset_t *sigmask);

select區(qū)別,是超時時間類型發(fā)生變化铺纽,支持納秒柬帕;其次,添加了信號屏蔽字狡门,pselect期間可屏蔽指定的信號陷寝,完成調(diào)用自動恢復(fù);

poll()


#include <poll.h>

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同與select使用三個位圖來表示三個fdset的方式其馏,poll使用一個 pollfd的指針實現(xiàn)凤跑。

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd結(jié)構(gòu)包含了要監(jiān)視的event和發(fā)生的event,不再使用select“參數(shù)-值”傳遞的方式叛复。同時仔引,pollfd并沒有最大數(shù)量限制(但是數(shù)量過大后性能也是會下降)。 和select函數(shù)一樣褐奥,poll返回后咖耘,需要輪詢pollfd來獲取就緒的描述符。

從上面看撬码,select和poll都需要在返回后儿倒,通過遍歷文件描述符來獲取已經(jīng)就緒的socket。事實上呜笑,同時連接的大量客戶端在一時刻可能只有很少的處于就緒狀態(tài)夫否,因此隨著監(jiān)視的描述符數(shù)量的增長,其效率也會線性下降蹈垢。

epoll

epoll是在2.6內(nèi)核中提出的慷吊,是之前的select和poll的增強版本。相對于select和poll來說曹抬,epoll更加靈活溉瓶,沒有描述符限制。epoll使用一個文件描述符管理多個描述符谤民,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個事件表中堰酿,這樣在用戶空間和內(nèi)核空間的copy只需一次。

套接字選項

SO_KEEPALIVE

tcp套接字闭抛悖活選項触创,若在指定時間內(nèi)(由系統(tǒng)配置決定)該套接字的任一方向都沒有數(shù)據(jù)交換,tcp就自動給對高端發(fā)送一個保持存活探測分節(jié)(keep-alive probe)为牍,這是對端必須相應(yīng)的tcp數(shù)據(jù)段哼绑,會導(dǎo)致以下三種情況:

  • 對端響應(yīng)ACK數(shù)據(jù)段岩馍,應(yīng)用進程不會得到通知(因為一切正常),再等待指定時間后抖韩,tcp將會再次發(fā)出一個敝鳎活探測分節(jié)茂浮;

  • 對端響應(yīng)RST双谆,以通知本端tcp:對端已崩潰且已重新啟動。該套接字的待處理錯誤會被置為ECONNRESET席揽,套接字本身會被關(guān)閉顽馋;

  • 對端無響應(yīng),tcp會嘗試再次發(fā)出被闲撸活探測分節(jié)寸谜;

    若根本無響應(yīng),則套接字的待處理錯誤置為ETIMEOUT属桦,套接字本身被關(guān)閉程帕;

    若收到ICMP錯誤,則會返回相應(yīng)的錯誤地啰,套接字本身被關(guān)閉;如常見的ICMP錯誤是"host unreachable"(主機不可達)讲逛,說明對端主機可能并沒有崩潰亏吝,只是不可達(如中間路由異常),待處理錯誤置為EHOSTUNREACH盏混;

SO_RCVBUF SO_SNDBUF

每個套接字都會有一個接收緩存區(qū)和發(fā)送緩存區(qū)蔚鸥,對于tcp緩存區(qū)大小限定tcp滑動窗口的大小,若發(fā)送數(shù)據(jù)長度查過該大小许赃,tcp就會丟棄該數(shù)據(jù)段止喷;對于udp,不存在流量控制混聊,若接收到的數(shù)據(jù)報超過緩存區(qū)大小弹谁,就會被丟棄

設(shè)置緩存區(qū)大小值時,注意函數(shù)調(diào)用順序句喜!

對于tcp预愤,窗口大小選項是在建立連接三次握手時交換得到,因此咳胃,客戶端端來說植康,需要在connect前設(shè)置;服務(wù)端來說展懈,需要在listen前設(shè)置销睁;

SO_REUSEADDR SO_REUSEPORT

SO_REUSEADDR套接字選項具有以下不同作用:

  • SO_REUSEADDR允許啟動一個監(jiān)聽服務(wù)器并捆綁眾所周知的端口供璧,即使以前建立的將該端口用作本地端口的連接仍存在;
  • 允許在同一端口上啟動同一個服務(wù)器的多個實例冻记,只要每個實例捆綁一個不同的IP地址即可睡毒;但不允許綁定同一個地址和端口在不同服務(wù)器上;
  • 允許單個進程綁定同一端口到多個套接字上檩赢,只要綁定不同的本地ip地址即可吕嘀;
  • 允許完全重復(fù)的綁定:如果一個ip地址和端口已綁定到某個套接字上,還可以綁定到另一個套接字上贞瞒,但前提是傳輸協(xié)議支持偶房,一般僅支持UDP;

getsockopt setsockopt

#include <sys/socket.h>

//若成功返回0军浆,否則-1
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
image.png

fcntl函數(shù)

與文件控制的名字相符棕洋,該函數(shù)執(zhí)行各種描述符控制操作,如修改非阻塞I/O乒融;


image.png

UDP 數(shù)據(jù)報傳輸協(xié)議

udp為無連接掰盘、無狀態(tài)、不可靠的傳輸層協(xié)議赞季,其中sokcet建立需要指定類型為SOCK_DGRAM;

常用的無連接udp傳輸函數(shù)流程如下:


image.png

具體的函數(shù)使用如下:

#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
                    struct sockaddr *fromaddr, socklen_t *addrlen);
ssize_t sendto(int sockfd, void *buff, size_t nbytes, int flags,
                const struct sockaddr *toaddr, socklen_t addrlen);

對于后兩個參數(shù)愧捕,recvfromaccept類似,都是接收對端的地址申钩;sendtoconnect類似次绘,都是發(fā)送或者建立連接的地址;每個tcp套接字都有接收/發(fā)送緩存區(qū)撒遣,但udp套接字只有接收緩存區(qū)邮偎,沒有發(fā)送緩存區(qū),從概念上說udp有數(shù)據(jù)就發(fā)送(不管對方是否接收)不會去緩存义黎,不需要發(fā)送緩存區(qū)禾进,且udp也可以發(fā)送空數(shù)據(jù);當udp接收緩存區(qū)滿時廉涕,繼續(xù)接收的數(shù)據(jù)會被丟棄泻云;可通過SO_RCVBUF選項來修改其大小狐蜕;

tcp也可以使用這兩個函數(shù)壶愤,不過不常見;

對于無綁定的的udp套接字來說馏鹤,sendto會觸發(fā)內(nèi)核自動綁定本地地址及臨時端口征椒,也可以通過bind綁定地址到套接字,則sendto就不需要指定發(fā)送地址(若指定就失斉壤邸)勃救;

已連接udp套接字

可使用connect函數(shù)來指定對端的地址碍讨,后續(xù)read send write等數(shù)據(jù)接收/發(fā)送函數(shù)就不需要指定地址;

connect函數(shù)udp不同于tcp會建立三次握手蒙秒,它不會發(fā)送任何消息勃黍,只是保存對端的地址,調(diào)用后立即返回晕讲,同時若在未綁定的套接字上覆获,內(nèi)核會綁定臨時端口;

使用connect已連接的udp與未連接的udp區(qū)別如下:

  • 已連接的udp不需要每次發(fā)送數(shù)據(jù)都指定對端地址瓢省;

  • 指定對端地址后弄息,對于不是該地址的數(shù)據(jù)報都不被應(yīng)用層接收;

  • 對于無連接的udp勤婚,數(shù)據(jù)發(fā)送后應(yīng)用層無法接收到對端的異常錯誤(內(nèi)核會接收到該消息)摹量,而已連接的udp會應(yīng)用層返回異步錯誤;

    port unreachable內(nèi)核會該icmp錯誤轉(zhuǎn)化為ECONNREFUSED錯誤馒胆,對于由err_sys函數(shù)輸出錯誤為connection resused缨称;

    image.png

    其他作用:

  • 再次調(diào)動connect可修改udp套接字上的地址,對于tcp不能兩次調(diào)用connect祝迂;

  • 斷開已連接的udp:將地址族成員sin_family修改為AF_UNSPEC睦尽,則會返回EAFUNSUPPORT錯誤,不過沒關(guān)系型雳;

使用已連接的udp套接字骂删,可減少每次連接套接字的步驟,因此四啰,相比無連接的udp套接字,性能提升粗恢;

對于connect調(diào)用只是告知內(nèi)核對端的地址柑晒,若是服務(wù)端未調(diào)用connect,且服務(wù)端多連接地址眷射,仍需要通過recvfrom來獲取客戶端的地址

其他

udp無流量控制匙赞,對于慢速服務(wù)端系統(tǒng),可能導(dǎo)致應(yīng)用層數(shù)據(jù)報丟包或者中間路由器丟失等妖碉;

可結(jié)合select函數(shù)判定udp是否可讀來接收發(fā)送數(shù)據(jù)涌庭;

使用建議

  • 對于單播和廣播必須使用udp
  • 對于簡單地請求應(yīng)答可以使用udp,不過需要添加錯誤檢測功能(如確認欧宜、超時及重傳機制)坐榆;
  • 對于海量數(shù)據(jù)(如文件發(fā)送)不建議使用udp(除了添加上一條的特性外,還需要添加窗口控制冗茸、擁塞避免和慢啟動等特性席镀,基本再造tcp)匹中;

SCTP 流量控制傳輸協(xié)議

SCTP(Stream Control Transmission Protocol,流量控制傳輸協(xié)議)是IETF(Internet Engineering Task Force豪诲,因特網(wǎng)工程任務(wù)組)在2000年定義的一個傳輸層(Transport Layer)協(xié)議顶捷,是提供基于不可靠傳輸業(yè)務(wù)的協(xié)議之上的可靠的數(shù)據(jù)報傳輸協(xié)議。

該協(xié)議被Linux2.6內(nèi)核版本吸納屎篱,但目前暫不支持macOS Windows系統(tǒng)服赎;

image.png

名字與地址轉(zhuǎn)換

image.png

對于gethostbyname gethostbyaddr getservbyname getservbyport getaddrinfo getnameinfo等函數(shù),是通過查詢本地/etc/hosts或者使用/etc/resolv.conf配置文件獲取dns服務(wù)器地址交播,進而通過udp查詢相應(yīng)的域名信息重虑;

具體函數(shù)用途:

gethostbyname通過主機名查詢ipv4地址;gethostbyaddr相反堪侯,通過ipv4地址查詢主機名嚎尤;

getservbyaddr通過服務(wù)名查詢端口;getservbyport通過端口查詢服務(wù)名伍宦;

getaddrinfogetnameinfo為協(xié)議無關(guān)轉(zhuǎn)換函數(shù)芽死,分別用于主機名字和ip地址之間和服務(wù)名字和端口號之間轉(zhuǎn)換;

gethostbyname gethostbyaddr為不可重入函數(shù)次洼,因為使用靜態(tài)變量存儲獲取信息关贵,不過可使用可重入版本gethostbyname_r gethostbyaddr_r版本;

gethostbyname()

#include <netdb.h>

struct hostent {
  char *h_name;         //查詢主機的規(guī)范名字卖毁,如dns查詢的CNAME記錄名稱
  char **h_aliases; //主機別名指針數(shù)組揖曾,以NULL為結(jié)束符
  int       h_addrtype; //主機地址類型(AF_INET)
  int       h_addrlen;  //主機地址長度(4)
  char **h_addr_list;//獲取ipv4地址指針數(shù)組,以NULL為結(jié)束符
}

struct hostent *gethostbyname(const char *hostname);

image.png

不同于其他套接字函數(shù)亥啦,該函數(shù)若失敗炭剪,不會設(shè)置errno值,而是設(shè)置h_errno全局變量翔脱,并可通過hstrerror函數(shù)返回具體的錯誤描述奴拦;*具體的錯誤如下:

  • HOST_HOT_FOUND
  • TRY_AGAIN
  • NO_RECOVERY
  • NO_DATA(等同NO_ADDRESS),表示無記錄

gethostbyaddr()

struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family)

gethostbyname相反届吁,通過ipv4地址返回主機名信息错妖;

getservbyname() getservbyport()

#include <netdb.h>

struct servent {
  char *s_name;     //服務(wù)名
  char **s_alias;   //別名列表
  int       s_port;     //端口名,網(wǎng)絡(luò)字節(jié)序
  char  *s_proto;   //使用的協(xié)議
}

//必須指定servname
//protoname為協(xié)議名稱疚沐,如"tcp" "udp"
struct servent *getservbyname(const char *servname, const char *protoname);
struct servent *getservbyport(int port, const char *protoname);

返回的servent結(jié)構(gòu)體中端口是以網(wǎng)絡(luò)字節(jié)序返回的暂氯,可直接用于套接字端口地址;

getaddrinfo()

#include <netdb.h>

struct addrinfo {
  int ai_flag;          //AI_XX
  int ai_family;        //AF_XX
  int ai_socktype;  //SOCK_XX
  int ai_protocol;  //0或者IPPROTO_XX for ipv4 or ipv6
  socklen_t ai_length;  //ai_addr長度
  char  *ai_cannoname;
  struct sockaddr *ai_addr; //獲取地址信息
  struct addrinfo *ai_next; //結(jié)構(gòu)體鏈表地址
}

//hints字段為需要獲取的指定信息
int getaddrinfo(const char *hostname, const char *service,
                const struct addrinfo *hints, struct addrinfo **result);
image.png

獲取的struct addrinfo **result為動態(tài)內(nèi)存分配亮蛔,使用完成需要使用freeaddrinfo函數(shù)釋放痴施,若失敗,可通過gai_strerror函數(shù)獲取失敗信息;

系統(tǒng)守護進程

syslogd

系統(tǒng)日志守護進程晾剖,macOS具體介紹見man syslogd

The syslogd server receives and processes log messages.  Several modules receive input messages through various channels, including UNIX domain sockets associated with the syslog(3), asl(3), and kernel printf APIs, and optionally on a UDP socket from network clients.

     The Apple System Log facility comprises the asl(3) API, a new syslogd server, the syslog(1) command-line utility, and a data store file manager, aslmanager(8).  The system supports structured and extensible messages, permitting advanced message browsing and management through search APIs and other components of the Apple system log facility.

大概意思是:syslogd守護進程主要用于接收和處理日志消息锉矢,包含unixt域套接字關(guān)聯(lián)的syslog asl(apple system log) 內(nèi)核日志, 以及udp套接字的網(wǎng)絡(luò)客戶端齿尽;

主要流程是:

  1. 系統(tǒng)啟動后由launchd fork出syslogd沽损,進程啟動后讀取/etc/syslog.conf /etc/asl.conf等配置文件;
  2. 創(chuàng)建`unix域套接字以監(jiān)聽系統(tǒng)日志相關(guān)的服務(wù)請求循头;
  3. 創(chuàng)建udp套接字绵估,綁定固定的端口(bsd系統(tǒng)應(yīng)該是514,mac不詳)卡骂;
  4. 打開路徑名/dev/klog国裳,內(nèi)核的任何消息都是這個設(shè)備的輸入;
image.png
注意:
  • syslog接口配置asl日志級別全跨,需要修改/etc/asl.conf其中默認級別是notice
    image.png
  • NSLog接口被設(shè)計為error log缝左,是ASL的高層封裝

NSLog會向ASL寫log,同時向Terminal寫log浓若,而且同時會出現(xiàn)在Console.app中(Mac自帶軟件渺杉,用NSLog打出的log在其中全部可見);不僅如此挪钓,每一次NSLog都會新建一個ASL client并向ASL守護進程發(fā)起連接是越,log之后再關(guān)閉連接。所以說碌上,當這個過程出現(xiàn)N次時倚评,消耗大量資源導(dǎo)致程序變慢也就不奇怪了;

建議使用CocoaLumberjack開源庫馏予;

關(guān)于mac下的syslog系統(tǒng)

NSLog效率低下的原因

inetd

該進程功能在Mac系統(tǒng)已被整合進launchd進程天梧;

daemon()

#include <stdlib.h>

//Unless the argument nochdir is non-zero, daemon() changes the current working directory to the root (/).
//Unless the argument noclose is non-zero, daemon() will redirect standard input, standard output, and standard error to /dev/null.
int daemon(int nochdir, int noclose);

系統(tǒng)提供了進程稱為守護進程的接口(不過在Mac上已被廢棄,但仍可使用)霞丧,具體流程與《Unix環(huán)境高級編程》中稱為守護進程流程一致呢岗,不過注意最后一步會使用openLog()函數(shù)啟動syslog日志接口,即創(chuàng)建unix套接字來連接syslogd蚯妇;

image.png

高級I/O函數(shù)

套接字I/O操作上設(shè)置超時時間的方法:

  • 調(diào)用alarm, 指定超時時間滿后產(chǎn)生SIGALARM信號

    該方法對于多線程正常使用信號非常困難

  • select超時作為定時器超時阻塞;

  • 使用套接字選項SO_RCVTIMEO SO_SNDTMEOrecvfrom sendto設(shè)置超時

    一旦設(shè)置選項暂筝,整個套接字都會生效箩言,優(yōu)勢是:一次性設(shè)置選項;

不可移植的超時方法:

  • /dev/poll文件

  • FreeBSD引入的kqueue接口(Mac繼承FreeBSD是支持的)

    本接口允許進程向內(nèi)核注冊描述所關(guān)注kqueue事件的事件過濾器(event filter)焕襟,除了與select所關(guān)注的類似文件I/O和超時外陨收,還有異步I/O、文件修改通知、進程跟蹤和信號處理务漩;見OSX/iOS中多路I/O復(fù)用總結(jié)

接收/發(fā)送函數(shù)

recv() send()
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
ssize_t send(int sockfd, void *buff, size_t nbytes, int flags);

read write類似拄衰,且前三個參數(shù)相同,不同是最后一個參數(shù)flags標志位饵骨,可設(shè)置為0翘悉;

image.png

readv() writev()
#include <sys/uio.h>

struct iovec {
  void *iov_base;   //緩存區(qū)的起始地址
  size_t iov_len;   //緩存區(qū)的長度
}

//iov為iovec結(jié)構(gòu)體數(shù)組的指針,iovcnt為結(jié)構(gòu)體的個數(shù)
ssize_t readv(int fields, const struct iovec *iov, int iovcnt);
ssize_t writev(int fields, const struct iovec *iov, int iovcnt);

read write函數(shù)類似居触,但其允許單個系統(tǒng)調(diào)用讀入或?qū)懗鲆粋€或多個緩存區(qū)妖混,分別稱為分散讀集中寫,用于*讀取應(yīng)用不連續(xù)內(nèi)存數(shù)據(jù)或?qū)⒉贿B續(xù)內(nèi)存數(shù)據(jù)寫出轮洋,而不需要多次調(diào)用read write制市;

recvmsg() sendmsg()
#include <sys/socket.h>

struct msghdr {
  void                  *msg_name;      //協(xié)議地址,用于指定或者接收
  socklen_t         msg_namelen;    //協(xié)議地址長度
  struct iovec *msg_iov;            //iovec結(jié)構(gòu)體數(shù)組指針
  int                   msg_iovlen;     //iovec結(jié)構(gòu)體數(shù)組長度
  void                  *msg_control;   //輔助數(shù)據(jù)地址
  socklen_t         msg_controllen;//輔助數(shù)據(jù)大小
  int                       msg_flags;          //標志位
}

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);

可取代read readv recvrecvfrom的通用I/O函數(shù)弊予;

image.png

unix域套接字

unix域套接字主要用于單主機本地通信祥楣,為常用的IPC通信之一,其具有如下優(yōu)勢:

  • 相比tcp/udp通信汉柒,unix域套接字不需要經(jīng)過網(wǎng)絡(luò)協(xié)議棧误褪,不需要經(jīng)過封包解包操作,速度快竭翠;且不需要指定ip地址振坚,而是通過本地文件;
  • 可跨進程傳遞描述符(不是簡單地傳遞描述符號斋扰,而是共享描進程打開的文件表項渡八,創(chuàng)建新的描述符);
  • 較新的實現(xiàn)把客戶端的憑證(用戶ID和組ID)提供給服務(wù)端传货,用于額外的安全檢查税产;

unix域套接字的地址結(jié)構(gòu)如下:

struct sockaddr_un {
  sa_family_t sun_family;       //AF_LOCAL(POSIX命令) or AF_UNIX,兩個值相同
  char              sun_path[104];  //路徑地址名稱炼蹦,需要絕對地址颓遏,若為相對地址,客戶/服務(wù)進程需要在同一路徑下
}

unix域套接字可重用tcp/ip通信的接口函數(shù)粮宛,且支持字節(jié)流類型(類似tcp窥淆,提供無邊界字節(jié)流接口)及數(shù)據(jù)報類型(類似udp,提供保留邊界記錄的不可靠的數(shù)據(jù)報服務(wù))巍杈,但具有如下不同:

  • unix套接字需要綁定路徑忧饭,而不是ip地址

    該路徑不能為空,且對于服務(wù)端bind前需要unlink刪除該路徑筷畦,否則會導(dǎo)致綁定失敶士恪刺洒;對于客戶端,該路徑需要明確是socket類型吼砂,且具有訪問權(quán)限逆航;

    bind綁定時會自動生成socket類型文件,該文件權(quán)限默認為0777渔肩,需要考慮到進程的umask文件屏蔽字因俐,可能存在差異;

    對于字節(jié)流類型赖瞒,connect前可不需要綁定(也可以綁定)女揭;但對于數(shù)據(jù)報類型,sendto發(fā)送函數(shù)內(nèi)核無法自動綁定地址栏饮,需要使用前綁定與服務(wù)端相同地址吧兔;

  • 對于字節(jié)流數(shù)據(jù)服務(wù),若connect連接時發(fā)現(xiàn)對端監(jiān)聽套接字隊列已滿袍嬉,則返回ECONNRESUSED錯誤境蔼;

    而對于tcp則不會ACK響應(yīng),客戶端將數(shù)次發(fā)送SYN分段重試伺通,直至超時或者路由不可達等錯誤箍土;

  • 對于未綁定的unix域套接字上發(fā)送數(shù)據(jù)報不會自動給這個套接字綁定路徑名,這不同于tcp/udp套接字:需要sendto或者connect連接對端時罐监,會自動綁定本地的臨時端口吴藻;

    對于字節(jié)流unix域套接字connect建立連接時弓柱,內(nèi)核會自動綁定客戶端的臨時路徑沟堡,不需要bind不同的路徑,但綁定也可以矢空;

    注意事項
    1. bind綁定地址長度航罗,可使用sizeof(struct sockaddr_un)結(jié)構(gòu)體的長度,或者使用包含sun_path字符串長度屁药,bind函數(shù)會自動獲取su_path路徑名粥血,但必須保證輸入長度不要超過結(jié)構(gòu)體的長度,防止棧溢出酿箭;

    2. bind綁定地址sun_path路徑名可為空字符串作為抽象路徑名复亏,但Mac上不可行;

      若為空字符串缭嫡,則等同于ipv4 INADDR_ANY或者ipv6 INADDR_ANY_INIT常值缔御,不過依然存在路徑名,只是不會創(chuàng)建該文件械巡,具體可參考抽象 unix 域套接字地址

    3. 若unix域套接字路徑文件被刪除是否有影響刹淌?

      若服務(wù)端及客戶端建立連接后,因為文件存在引用計數(shù)概念讥耗,只要有進程持有該文件有勾,內(nèi)核直至該文件引用計數(shù)為0時才會釋放刪除它,因此無影響古程!

      若對于服務(wù)端綁定監(jiān)聽后而客戶端未連接時刪除蔼卡,則客戶端無法連接到服務(wù)端;

      若服務(wù)端停止后文件域套接字文件未刪除挣磨,客戶端connect連接雇逞,則無法連接,因為連接成功需要當前有一個打開的綁定了域套接字文件的域套接字茁裙;

setsockpair()

socketpair函數(shù)可以創(chuàng)建兩個連接起來的unix域套接字:

#include <sys/socket.h>
int socketpair(int family, int type, int protocol, int sockfd[2]);

socketpair 的參數(shù)中family必須為AF_LOCAL塘砸,protocol必須為0,type可以為SOCK_STREAMSOCK_DGRAM晤锥,新創(chuàng)建的兩個套接字描述符將作為sockfd[0]sockfd[1]返回掉蔬。類似管道形式,但為流管道矾瘾。

傳遞描述符

當我們需要傳遞描述符時女轿,通常可以使用方法有:

  1. fork調(diào)用返回以后壕翩,子進程共享父進程的所有描述符
  2. exec調(diào)用執(zhí)行后蛉迹,所有的描述符通常保持打開狀態(tài)

第一種方式里,我們可以把描述符從父進程傳遞到子進程放妈,然而我們也可能需要在子進程傳遞描述符到父進程北救。unix系統(tǒng)提供了用于從一個進程向其他任意進程傳遞描述符的方式,而這兩個進程不需要有任何親緣關(guān)系大猛。這種技術(shù)要求在兩個進程之間創(chuàng)建一個uds扭倾,然后使用sendmsg通過這個uds發(fā)送特殊結(jié)構(gòu)的消息。這個特殊的消息會由內(nèi)核處理挽绩,把打開的描述符從發(fā)送進程傳遞到接收進程膛壹。

通過uds傳遞描述符的步驟具體如下:

  1. 創(chuàng)建一個字節(jié)流或數(shù)據(jù)報的uds。這可以通過調(diào)用socketpair然后父子進程之間的連接唉堪;也可以使用套接字API模聋。通常建議使用字節(jié)流套接字而不是數(shù)據(jù)報套接字,因為使用數(shù)據(jù)報套接字并沒有什么好處唠亚,反而還存在數(shù)據(jù)報被丟棄的可能链方。
  2. 發(fā)送端打開描述符。uds可以傳遞各種類型的描述符灶搜,而不是僅包括文件描述符祟蚀。
  3. 發(fā)送端進程創(chuàng)建一個msghdr的結(jié)構(gòu)工窍,其中含有待傳遞的描述符,然后調(diào)用sendmsg將其發(fā)送出去前酿。發(fā)送一個描述符會使其引用計數(shù)加一患雏。posix規(guī)定需要作為輔助數(shù)據(jù)發(fā)送描述符;
  4. 接收端進程調(diào)用recvmsg在創(chuàng)建的uds上接收描述符罢维。這個過程會在接收進程創(chuàng)建一個新的描述符淹仑,然后將其指向和發(fā)送進程發(fā)送的描述符指向的同一個內(nèi)核文件選項。所以接收端收到的描述符不同于發(fā)送端發(fā)送端描述符時很正常的肺孵。

msghdr的結(jié)構(gòu)定義:

/*
 * [XSI] Message header for recvmsg and sendmsg calls.
 * Used value-result for recvmsg, value only for sendmsg.
 */
struct msghdr {
    void        *msg_name;  /* [XSI] optional address */
    socklen_t   msg_namelen;    /* [XSI] size of address */
    struct      iovec *msg_iov; /* [XSI] scatter/gather array */
    int     msg_iovlen; /* [XSI] # elements in msg_iov */
    void        *msg_control;   /* [XSI] ancillary data, see below */
    socklen_t   msg_controllen; /* [XSI] ancillary data buffer len */
    int     msg_flags;  /* [XSI] flags on received message */
};

具體的例子就暫時不列舉了匀借。

驗證發(fā)送者的身份

可以用uds傳遞的另一種輔助數(shù)據(jù)就是用戶憑證。用戶憑證的數(shù)據(jù)結(jié)構(gòu)在不同的操作系統(tǒng)中并不一致平窘,這里就不再詳細介紹了吓肋。不過需要使用<sys/soket.h>定義的cmsgcred結(jié)構(gòu)體傳遞憑證!

非阻塞I/O

套接字默認是阻塞式瑰艘,若使用非阻塞式蓬坡,需要通過fcntl函數(shù)設(shè)置套接字為O_NONBLOCK,具體如下:

//獲取當前套接字標志
int flags = fcntl(sockfd, F_GETLF, 0);
if (flags < 0) {
  printf("fcntl err:%s", strerror(errno));
}
//設(shè)置當前套接字標志
fcntl(sockfd, F_SETFL, flags|O_NONBLOCK);

具體分類如下:

  • 輸入操作磅叛,如read readv recvfrom recv recvmsg五個函數(shù)

    對于阻塞式操作屑咳,tcp類型套接字需要等待內(nèi)核接收緩存區(qū)有數(shù)據(jù)可讀(單個字節(jié)或多個字節(jié),可通過指定MSG_WAITALL標志位[需要支持]來指定讀取固定數(shù)目字節(jié))弊琴,且從內(nèi)核緩存區(qū)拷貝至用戶緩存區(qū)才會返回兆龙,否則會當前進程會睡眠,直至有數(shù)據(jù)可讀敲董;udp類型紫皇,需要等待有數(shù)據(jù)報可讀;

    對于非阻塞式腋寨,若無數(shù)據(jù)可讀都會返回錯誤EWOULDBLOCK;

  • 輸出操作聪铺,如write writev sendto send sendmsg五個函數(shù)

    對于阻塞式,tcp類型需要內(nèi)核發(fā)送緩存區(qū)有空間寫萄窜,且將用戶緩存區(qū)拷貝至內(nèi)核緩存區(qū)才會返回(返回的為寫入緩存區(qū)的字節(jié)數(shù))铃剔,否則進程會睡眠;

    對于非阻塞式查刻,tcp類型會返回EWOULDBLOCK錯誤键兜;

    udp類型不存在發(fā)送緩存區(qū)概念,會直接發(fā)送數(shù)據(jù)報穗泵;

  • 接受外來連接操作普气,如accept函數(shù)

    對于阻塞式,會一直等待已完成連接隊列存在連接佃延,否則進程睡眠现诀;

    對于非阻塞式夷磕,若尚無新的連接會返回EWOULDBLOCK;

  • 發(fā)出連接操作,如connect函數(shù)

    對于阻塞式仔沿,tcp類型connect函數(shù)會直至三次握手完成才會返回企锌;udp類型實質(zhì)是內(nèi)核保存對端的地址和端口,立即返回(不會阻塞)于未;

    對于非阻塞,若tcp類型connect連接未完成會返回EINPROGRESS錯誤陡鹃,不同于上面的套接字函數(shù)烘浦,通常單主機情況(客戶和服務(wù)端在同一主機)會立即連接返回;

ioctl操作

#include <unistd.h>

int ioctl(int fd, int request, ...);//成功返回0萍鲸,否則-1

網(wǎng)絡(luò)程序經(jīng)常使用ioctl獲取所在主機全部網(wǎng)絡(luò)接口的信息闷叉,包括:接口地址、是否支持廣播脊阴、是否支持多播握侧,等待;

kcp快速可靠協(xié)議

KCP是一個快速可靠協(xié)議嘿期,能以比 TCP浪費10%-20%的帶寬的代價品擎,換取平均延遲降低 30%-40%,且最大延遲降低三倍的傳輸效果备徐。純算法實現(xiàn)萄传,并不負責底層協(xié)議(如UDP)的收發(fā),需要使用者自己定義下層數(shù)據(jù)包的發(fā)送方式蜜猾,以 callback的方式提供給 KCP秀菱。 連時鐘都需要外部傳遞進來,內(nèi)部不會有任何一次系統(tǒng)調(diào)用蹭睡。

整個協(xié)議只有 ikcp.h, ikcp.c兩個源文件衍菱,可以方便的集成到用戶自己的協(xié)議棧中。也許你實現(xiàn)了一個P2P肩豁,或者某個基于 UDP的協(xié)議脊串,而缺乏一套完善的ARQ可靠協(xié)議實現(xiàn),那么簡單的拷貝這兩個文件到現(xiàn)有項目中清钥,稍微編寫兩行代碼洪规,即可使用。

tcp相比循捺,同樣擁有發(fā)送確認斩例、窗口控制、慢啟動及擁塞控制从橘,但其可關(guān)閉慢啟動及擁塞控制念赶,并且改變了RTO超時時間策略础钠,可選擇性重傳丟失包,可調(diào)節(jié)延時發(fā)送ACK叉谜;TCP可靠簡單旗吁,但是復(fù)雜無私,所以速度慢停局。KCP盡可能保留UDP快的特點下很钓,保證可靠。可靠UDP董栽,KCP協(xié)議快在哪码倦?

image.png

具體應(yīng)用層使用:

kcp純算法實現(xiàn),需要外部傳入當前時間戳及底層udp發(fā)送接口即回調(diào)函數(shù)(用于kcp_flush調(diào)用發(fā)送數(shù)據(jù)報)锭碳,外部需要間隔10ms或者100ms來循環(huán)調(diào)用kcp_update來更新kcp狀態(tài)袁稽,kcp_update會觸發(fā)調(diào)用kcp_flush來負責數(shù)據(jù)的超時重傳、快速重傳擒抛、選擇性重傳推汽、數(shù)據(jù)正常發(fā)送、數(shù)據(jù)報響應(yīng)等歧沪;

kcp_send負責將應(yīng)用層用戶數(shù)據(jù)進行分片(若超過mtu)歹撒,并將其snd_queue發(fā)送隊列中;

kcp_input負責將recv_buff接收緩存中數(shù)據(jù)解包并重新組裝更新接收隊列recv_queuq可靠地供應(yīng)用層獲取數(shù)據(jù),其中包含了數(shù)據(jù)類型诊胞、長度校驗栈妆,對數(shù)據(jù)報類型IKCP_CMD_PUSH IKCP_CMD_ACK IKCP_CMD_WASK IKCP_CMD_WINS類型數(shù)據(jù)報處理,并進行流量控制及擁塞控制厢钧;

kcp_recv負責將recv_queue接收隊列中的數(shù)據(jù)進行合并鳞尔,并拷貝至用戶緩存區(qū);

image.png

image.png

源碼解析參考:

KCP: 快速可靠的ARQ協(xié)議

KCP原理及源碼解析

網(wǎng)絡(luò)傳輸協(xié)議kcp原理解析

帶外數(shù)據(jù)

許多傳輸層有帶外數(shù)據(jù)(out-of-band data)概念早直,有時也成為經(jīng)加速數(shù)據(jù)(expedited data)寥假。

主要用于已連接某端發(fā)生了重要事情,能迅速通告對端霞扬,“迅速”是指通知應(yīng)該在已經(jīng)排隊等待發(fā)送的任何“普通”數(shù)據(jù)前發(fā)送糕韧;

tcp并沒有真正的帶外數(shù)據(jù),但其提供了緊急模式緊急指針喻圃,udp未實現(xiàn)帶外數(shù)據(jù)概念萤彩;

使用

發(fā)送帶外數(shù)據(jù):

send(sockfd, ‘a(chǎn)’, 1, MSG_OOB);//其中MSG_OOB指帶外標記

發(fā)送進程通過send調(diào)用指定帶外標志MSG_OOB來發(fā)送帶外數(shù)據(jù),不過該調(diào)用只有最后一個字節(jié)為帶外數(shù)據(jù)斧拍,即帶外數(shù)據(jù)只有一個字節(jié)雀扶;接收進程tcp收到新的緊急指針后,可通過SIGURG信號,或者通過select異常處理 來接收通知愚墓;

帶外數(shù)據(jù)未廣泛使用予权,一般用于心跳機制;

原始套接字

原始套接字提供了tcp udp無法提供的能力:

  • 可以讀或?qū)?icmpv4 icmpv6 igmpv4等分組浪册,直接在用戶進程處理扫腺;
  • 可以讀寫內(nèi)核不處理其協(xié)議字段的ipv4數(shù)據(jù)報(大多數(shù)內(nèi)核只處理icmp igmp tcp udp數(shù)據(jù)報);
  • 進程可以使用IP_HDRINCL套接字選項自行構(gòu)造ipv4首部村象;

具體使用:

int sockfd = socket(AF_INET, SOCK_RAW, protocol)笆环;//其中protocol參數(shù)是IPPROTO_XXXX的某個常值,如IPPROTO_ICMP

只要超級用戶才能創(chuàng)建原始套接字厚者,防止普通用戶往網(wǎng)絡(luò)寫自行構(gòu)造的IP數(shù)據(jù)報躁劣,但Mac系統(tǒng)自帶的ping程序不需要提權(quán)操作,具體原因是:

socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);

其使用了SOCK_DGRAM類型及IPPROTO_ICMP選項籍救,

這種創(chuàng)建這種套接字是合法的,但并非所有的平臺都能創(chuàng)建渠抹,這還是要取決于內(nèi)核/proc/sys/net/ipv4/ping_group_range 這個屬性值蝙昙,是一對整數(shù),指定了允許使用 ICMP 套接字的組 ID的范圍(可修改梧却,需要權(quán)限)奇颠。在Linux一些版本比如Ubuntu,centos放航,這個默認值是0 1,意味著沒人能夠使用這個特性烈拒,在Android上這個范圍是0 2147483647,意味著進程都可以創(chuàng)建這種套接字。Mac也是可以的广鳍,所以也說明了為什么ubuntu下的ping是帶s位的荆几,而Mac和Android設(shè)備上的ping是不用帶的,因為使用這種socket已經(jīng)可以達到ping的功能赊时。
————————————————
原文鏈接:https://blog.csdn.net/aa642531/java/article/details/85461294

其中原始套接字還可以使用bind綁定本地地址(僅能綁定本地地址吨铸,因為原始套接字沒有端口的概念),如果不綁定祖秒,內(nèi)核就會自動綁定外出接口的地址诞吱;

原始套接字還可以使用connect函數(shù)來確定目的地址(僅目的地址,原始套接字沒有端口概念)竭缝,后續(xù)就可以使用send write等函數(shù)取代sendto ;

設(shè)定IP_HDRINCL選項:

const int on = 1;
setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on));

原始套接字輸出

image.png

原始套接字還提供了一個非常有用的參數(shù)IP_HDRINCL:
1房维、當開啟該參數(shù)時:我們可以從IP報文首部第一個字節(jié)開始依次構(gòu)造整個IP報文的所有選項,但是IP報文頭部中的標識字段(設(shè)置為0時)和IP首部校驗和字段總是由內(nèi)核自己維護的抬纸,不需要我們關(guān)心咙俩。
2、如果不開啟該參數(shù):我們所構(gòu)造的報文是從IP首部之后的第一個字節(jié)開始湿故,IP首部由內(nèi)核自己維護暴浦,首部中的協(xié)議字段被設(shè)置成調(diào)用socket()函數(shù)時我們所傳遞給它的第三個參數(shù)溅话。

原始套接字輸入

image.png

調(diào)試技術(shù)

image.png

tcpdump wireshark抓包工具 抓取完整的數(shù)據(jù),dtrace系統(tǒng)調(diào)用跟蹤工具歌焦,netstat 查看哪些ip地址及端口在使用飞几、tcp狀態(tài)及路由信息等, lsof查看端口占用及所屬進程独撇、進程id屑墨、用戶、描述符fd纷铣、類型卵史、文件的大小或偏移、協(xié)議類型及名稱等搜立;
image.png

其它的如ping icmp協(xié)議查看對方是否可達以躯,route 添加或刪除路由表,networksetup修改靜態(tài)路由啄踊,等忧设;

源碼下載編譯

下載地址:www.unpbook.com,也可以使用其他人構(gòu)建的git分支颠通,如https://github.com/DingHe/unpv13e.git(

編譯運行

Execute the following from the src/ directory:

    ./configure    # try to figure out all implementation differences

    cd lib         # build the basic library that all programs need
    make           # use "gmake" everywhere on BSD/OS systems

    cd ../libfree  # continue building the basic library
    make

    cd ../libroute # only if your system supports 4.4BSD style routing sockets
    make           # only if your system supports 4.4BSD style routing sockets

    cd ../libxti   # only if your system supports XTI
    make           # only if your system supports XTI

    cd ../intro    # build and test a basic client program
    make daytimetcpcli
    ./daytimetcpcli 127.0.0.1

注意:網(wǎng)上教程需要將libunp.a unp.h靜態(tài)庫及頭文件放置/usr/local/lib /usr/local/include址晕,其實makefile已經(jīng)鏈接了當前目錄文件;

遇到的問題

  1. 函數(shù)定義沖突
    image.png

    屏蔽inet_ntop.c中的inet.h鏈接系統(tǒng)頭文件顿锰,再次make編譯谨垃;
  2. libxti not found
    編譯說明只需要支持XTI系統(tǒng)才需要,暫不編譯硼控;
  3. Ld:symbol(s) not found for architecture x86_64
    image.png

    查看makefile文件系統(tǒng)已經(jīng)鏈接靜態(tài)庫libunp.a刘陶,且nm libunp.a查看靜態(tài)庫符號存在該符號;
?  intro git:(master) ? file ../libunp.a
../libunp.a: current ar archive

?  intro git:(master) ? lipo -info ../libunp.a
Non-fat file: ../libunp.a is architecture: x86_64

通過file查看靜態(tài)庫文件類型為ar archive(壓縮格式)牢撼,通過lipo -info查看支持x86_64易核;

image.png

查看libunp.a內(nèi)部包含的*.o文件,其中包含了error.o浪默,但仍然存在符號找不到情況牡直。

直接鏈接error.o文件,編譯通過纳决;

gcc -I../lib -g -O2 -D_REENTRANT -Wall -o daytimetcpcli daytimetcpcli.o ../lib/error.o

通過添加libunp.a靜態(tài)庫并改變與error.o的鏈接順序碰逸,都編譯通過!阔加,因此排除gcc鏈接問題饵史,應(yīng)該是libunp.a靜態(tài)庫問題,導(dǎo)致ld無法鏈接該靜態(tài)庫;

注意:ld:warning: ignoring file提示胳喷,因此ld鏈接時忽略了靜態(tài)庫湃番!

image.png

網(wǎng)上遇到了類似問題:在macOS-Mojave上編譯Lua失敗的經(jīng)歷
image.png

You must use the correct ar (archiver), e.g.:
make CXX=o64-clang++ AR=x86_64-apple-darwin1X-ar
O?therwises it uses the system ar and won’t work.

StackOverFlow 上也有一個相似的問題

說是使用的 ar 命令不對(或者是說 GNU 和 macOS ar命令的行為不太一樣)。隱約記得我是裝了一個 binutils 的吭露,難道被覆蓋了吠撮。
于是查了一下:
which ar ranlib
/usr/local/opt/binutils/bin/ar
/usr/local/opt/binutils/bin/ranlib

~/.zshrc環(huán)境變量配置文件中發(fā)現(xiàn)指定了ar的鏈接路徑;

image.png

具體問題:猜測是GNU的ar/ranlib 與 macos工具不一致導(dǎo)致

【解決】

修改src/Make.defines中的ranlib路徑及l(fā)ib/Makefile中的ar路徑讲竿;


image.png
  1. connection refused
    image.png

    原因是獲取時間的服務(wù)器(port:13)沒有運行泥兰,intro/下有個daytimetcpsrv.c文件在另一個終端窗口下make后運行該服務(wù)器程序即可,如下题禀;
    image.png

小知識

1. gcc鏈接順序問題

gcc鏈接存在鏈接順序問題鞋诗,如靜態(tài)庫(.a就是包含了許多.o文件)**

gcc -l 解釋如下:
-l library
Search the library named library when linking. (The second alter-
native with the library as a separate argument is only for POSIX
compliance and is not recommended.)

? It makes a difference where in the command you write this option;
? the linker searches and processes libraries and object files in the
? order they are specified. Thus, foo.o -lz bar.o searches library z
? after file foo.o but before bar.o. If bar.o refers to functions in
? z, those functions may not be loaded.

這句話翻譯過來的意思就是說,如果你的庫在鏈接時安排的順序是:foo.o -lz bar.o迈嘹。那么gcc的鏈接器先搜索庫foo削彬,然后是z庫,然后是bar庫秀仲。

這樣就帶來一個問題融痛,如果庫bar調(diào)用了庫z里面的函數(shù),但是鏈接器是先搜索的庫z啄育,這時候并沒有發(fā)現(xiàn)庫bar調(diào)用庫z啊酌心,所以就不會把庫z中的這部分函數(shù)體挑出來進行鏈接拌消。

2.ar ranlib

ar 命令

這個命令是將多個 obj 文件打包成一個靜態(tài) .a 庫文件挑豌。其用法類似于壓縮命令啊。

ranlib

這個命令會將 ar 打包后的文件墩崩,里面所有的 object 文件定義的符號氓英,生成一個索引存在里面○谐铮可以加快鏈接速度铝阐。 GNU 的 ranlib 命令是和 ar -s 命令等價的。

概念

  • 包裹函數(shù)

    對系統(tǒng)函數(shù)錯誤自定義的錯誤處理函數(shù)铐拐,用于處理系統(tǒng)函數(shù)錯誤(如打印錯誤信息徘键,崩潰處理等);

  • 協(xié)議數(shù)據(jù)單元(protocol date unit, PDU)

    計算機網(wǎng)絡(luò)各層對等實體間交換的單位信息稱為協(xié)議數(shù)據(jù)單元遍蟋,分節(jié)(segment)就是對應(yīng)tcp傳輸層的PDU吹害,應(yīng)用層交換的PDU稱為應(yīng)用數(shù)據(jù),網(wǎng)絡(luò)層交換的PDU稱為ip數(shù)據(jù)報虚青,鏈路層交換的PDU稱為數(shù)據(jù)幀

  • accept() close()

    accpet()內(nèi)核會執(zhí)行tcp的三次握手它呀,close()內(nèi)核會執(zhí)行tcp的四次揮手;

  • 原始套接字(raw socket)

    應(yīng)用層繞過傳輸層直接使用網(wǎng)絡(luò)層的套接字;

  • 與數(shù)據(jù)鏈路層通信應(yīng)用

    tcpdump或者BSD分組過濾器(BSD packet filter, BPF)接口(通過該接口在源自berkeley的內(nèi)核中找到 )或者數(shù)據(jù)鏈路層提供者接口(datalink provider interface, DLPI)通常隨svr4內(nèi)核提供纵穿, 直接與數(shù)據(jù)鏈路層通信下隧;

  • 保護消息邊界

    保護消息邊界,就是指傳輸協(xié)議把數(shù)據(jù)當作一條獨立的消息在網(wǎng)上傳輸谓媒,接收端只能接收獨立的消息淆院,也就是說存在保護消息邊界,接收端一次只能接收發(fā)送端發(fā)出的一個數(shù)據(jù)包. 而面向流則是指無保護消息保護邊界的,如果發(fā)送端連續(xù)發(fā)送數(shù)據(jù), 接收端有可能在一次接收動作中,會接收兩個或者更多的數(shù)據(jù)包篙耗。

  • FIN

    無論進程異常退出還是正常退出迫筑,內(nèi)核會將進程所有打開的文件描述符全部關(guān)閉,因此所有的tcp連接未關(guān)閉的宗弯,如處于close_wait脯燃、FIN_WAIT2狀態(tài)的,都會發(fā)出FIN數(shù)據(jù)包蒙保,最終FIN_WAIT2狀態(tài)就會轉(zhuǎn)移為TIME_WAIT狀態(tài)辕棚,確認主動方發(fā)出的ACK確認包已經(jīng)抵達被動方;

  • TIME_WAIT

    該狀態(tài)存在的理由:

    • 可靠地實現(xiàn)tcp全雙工連接的終止邓厕;

    • 允許老的重復(fù)分節(jié)在網(wǎng)絡(luò)中消逝逝嚎;

      使用上一次的tcp連接,若tcp處于TIME_WAIT狀態(tài)详恼,則創(chuàng)建連接會失敳咕贪磺;除非新的連接SYN序列號大于前一化身的結(jié)束序列號针贬;

  • 套接字對(socket pair)

    一個tcp連接的套接字對是一個定義該鏈接的兩個端點的四元組:本地ip地址颠蕴、本地端口东涡、遠端ip地址祖今、遠端端口折柠;標識每個端點的兩個值(ip和端口)通常稱為一個套接字蚣抗;

參考資料

0-MacOS Catalina下Unix網(wǎng)絡(luò)編程環(huán)境搭建詳細教程

附錄

https://github.com/FengyunSky/notes/blob/master/study/system/unix/kcp.tar
https://github.com/FengyunSky/notes/blob/master/study/system/unix/unpv13e.tar

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末道偷,一起剝皮案震驚了整個濱河市玖雁,隨后出現(xiàn)的幾起案子更扁,更是在濱河造成了極大的恐慌,老刑警劉巖赫冬,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浓镜,死亡現(xiàn)場離奇詭異,居然都是意外死亡劲厌,警方通過查閱死者的電腦和手機膛薛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脊僚,“玉大人相叁,你說我怎么就攤上這事遵绰。” “怎么了增淹?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵椿访,是天一觀的道長。 經(jīng)常有香客問我虑润,道長成玫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任拳喻,我火速辦了婚禮哭当,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘冗澈。我一直安慰自己钦勘,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布亚亲。 她就那樣靜靜地躺著彻采,像睡著了一般。 火紅的嫁衣襯著肌膚如雪捌归。 梳的紋絲不亂的頭發(fā)上肛响,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音惜索,去河邊找鬼特笋。 笑死,一個胖子當著我的面吹牛巾兆,可吹牛的內(nèi)容都是我干的猎物。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼臼寄,長吁一口氣:“原來是場噩夢啊……” “哼霸奕!你這毒婦竟也來了溜宽?” 一聲冷哼從身側(cè)響起吉拳,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎适揉,沒想到半個月后留攒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡嫉嘀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年炼邀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剪侮。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡拭宁,死狀恐怖洛退,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情杰标,我是刑警寧澤兵怯,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站腔剂,受9級特大地震影響媒区,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜掸犬,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一袜漩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧湾碎,春花似錦宙攻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至呻顽,卻和暖如春雹顺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背廊遍。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工嬉愧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人喉前。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓没酣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親卵迂。 傳聞我的和親對象是個殘疾皇子裕便,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容