第4章 基本tcp套接字編程
4.1 各種套接字api(重要)
4.1.1 socket()
用於創建一個套接字描述符,這個描述符指明的是tcp還是udp,同時還有ipv4還是ipv6
-
family
主要是指明的協議族,AF_INET
:ipv4、AF_INET6
:ipv6 、AF_LOCAL
:unix域協議、AF_ROUTE
:路由套接字、AF_KEY
秘鑰套接字網絡編程中主要還是前兩種
-
type
指明套接字類型,主要是數據報,還是流式,原始套接字SOCK_STREAM
:流式,SOCK_DGRAM
:報式 、SOCK_SEQPACKET
有序分組套接字、SOCK_RAW
原始套接字 -
protocol
是控制協議,通常位0,表示由前兩個參數組合出來的協議的默認類型
AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY | |
---|---|---|---|---|---|
SOCK_STREAM | TCP|SCTP | TCP|SCTP | 是 | ||
SOCK_DGRAM | UDP | UDP | 是 | ||
SOCK_SEQPACKET | SCTP | SCTP | 是 | ||
SOCK_RAW | ipv4自己填寫 | ipv6自己填寫 | 是 | 是 |
其中AF_
和PF_
開頭的都有,但是前一個表示地址族,後一個表示協議族。但是後一個現在很少用。
socket()
創建的是主動套接字。現在獲得的套接字還不能夠像普通的文件描述符一樣進行讀寫,套接字描述符需要綁定本端套接字(bind)和對端套接字(connect,或是send)。
4.1.2 connect()
將一個套接字描述符與一對套接字地址綁定。這樣就使得套接字像是一個打開文件獲得的文件描述符一樣,可以通過操作這個描述符來操作與一個地址之間的通信數據。
#include <sys/socket.h> ? int connect(int sfd, const struct sockaddr *sevaddr, socklen_t addrlen); //成功0,出錯-1,設置errno
-
sfd
是由socket()
函數獲得的套接字 -
sockaddr *sevaddr
可以看出來,傳入的參數只能是sockaddr
類型的,所以需要強轉另外通過名字可以知道,綁定的是一個服務器地址。
-
addrlen
套接字地址結構的長度,有了這個長度,內核才知道,要復制多少數據。
4.1.2.1 阻塞套接字connect
connect()
通常都是客戶端調用去連接服務器,調用connect()
以後,本機就與sevaddr
指定的主機進行連接。如果是tcp那麽會觸發三次握手, 當套接字是阻塞套接字時,該函數僅僅在出錯或是建立成功以後才會返回,出錯的情況有:
-
直接死在arp上,返回-1,errno設置ETIMEOUT,或是死在路由器的arp上是
UN**REACH
-
本機發送syn,且重發,並等待總共75秒後,沒有收到syn分節的回應。
ETIMEDOUT
-
本機發送syn分節,收到
rst
復位分節,表示在服務器的指定端口上,沒有進程在等待連接。這是一種 硬錯誤,也就是不是重試能夠解決的。函數返回ECONNREFUSED
能夠收到
rst
分節的情況有(這是拓展)-
對應服務器的端口上,沒有進程在等待連接,也就是沒有
listen()
-
tcp想要終止一條連接,本端EPOLLERR
-
tcp收到了一條不存在連接上的數據,也就是,收到一條陌生的數據,而且該數據不是syn分節
-
-
本機發送syn分節,但是在syn分節在到服務器的中途中的某個 路由器上引發一個目的不可達的
icmp
錯誤,是一種軟錯誤,可以通過重發解決。本機接收到icmp
報文以後,重試,如果在75秒內沒有收到syn分節則返回EHOSTUNREACH
或是ENETUNREACH
錯誤註意這裏僅僅只路由器死在arp的時候返回的icmp會這樣處理,直接交付數據報死在arp上,是ETIMEDOUT
目的不可達的原因(代碼默認ipv4):
-
主機不可達1,是由路由器或是本機,當本機要求直接交付數據(子網),但是該主機已經離線,死在arp上,(這樣貌似是本機產生icmp),或是路由器也死在arp上,那麽就會發送icmp。
其中icmpv6中,將直接交付產生的目的不可達,單獨作為一個代碼,0
-
禁止通信3,通常由路由器丟棄流量導致,通常情況下,不會產生這類報文,防火墻直接丟棄,不產生。
-
端口不可達3,通常是udp中,數據包的目的端口,沒有進程在監聽。返回端口不可達的icmp
代碼和禁止通信一樣
-
數據報大於MTU但是設置部分片4,產生目的不可達icmp
這種情況下,該函數會:
-
直接返回
-
???
但是
ENETUNREACH
不可達已經過時了,應該將兩種錯誤看作一種處理。 -
當connect失敗的時候,必須關閉套接字,不能再次對同一個套接字進行connect**
4.1.2.2 非阻塞套接字connect
非阻塞connect套接字的作用:
-
完成一個connect要花費RTT時間,而RTT波動範圍很大,從局域網上的幾個毫秒甚至是廣域網上的幾秒,這段時間也許有我們要執行的其他處理工作可以執行。
-
可以使用這個技術同時建立多個連接。
-
許多connect的超時實現以75秒為默認值,如果應用程序想自定義一個超時時間,就是使用非阻塞的connect.
在一個非阻塞的套接字上調用connect()
,connect()
會立即返回EINPROGRESS
錯誤(非本機),0(本機),但是已經發起的TCP三次握手繼續進行。
通常,非阻塞的套接字,我們不會直接去處理connect()
後的套接字,而是在connect()
後,將該套接字,放入IO復用的api中。
非阻塞connect套接字實現時需要註意的細節:
-
連接到同一主機上,connect會立即完成,我們必須處理這種情形
調用
connect
,如果返回0,表示連接已經完成,如果返回-1,那麽期望收到的錯誤errno
是EINPROGRESS
,連接建立已經 啟動,但是尚未完成。 -
POSIX關於select和非阻塞connect的以下兩個規則:
-
連接成功,描述符會變成可寫 (連接建立時,寫緩沖區空閑,所以可寫)
-
連接建立遇到錯誤時,描述符變為可讀可寫(由於有未決的錯誤,從而可讀又可寫)通常是EPOLLERR帶上之前監聽的事件,嚴格上如果只監聽了讀,那麽就沒有
-
完整的io復用流程位:
參考
-
創建非阻塞套接字,或是,創建以後調用fcntl把套接字設置為非阻塞
-
調用connect,如果返回0,表示連接已經完成,如果返回-1,那麽期望收到的錯誤是EINPROGRESS,連接建立已經 啟動,但是尚未完成。
-
調用select,並且設置超時時間
-
超時處理
如果select返回0,超時發生,那麽返回
ETIMEOUT
錯誤給調用者,並且關閉套接字,防止已經啟動的三路握手繼續下去。 -
連接錯誤和成功處理
如果描述符變為可讀或可寫,通過
getsockopt(sockfd,SOL_SOCKET,SO_ERROR,(char *)&error,&len)
獲取的error 值,如果建立連接時遇到錯誤,則errno 的值是連接錯誤所對應的errno值,比如ECONNREFUSED
,ETIMEDOUT
等連接成功:getsockopt
返回0 連接失敗:getsockopt
返回0, 並且獲取相應的錯誤。muduo使用的是該方法,直接監聽讀事件,當由EPOLLERR事件的時候,直接關閉.
另一種方法是參考:
Linux環境下是有效的:
再次調用
connect
,相應返回失敗,如果錯誤errno是EISCONN
,表示socket連接已經建立,否則認為連接失敗。
4.1.2.3 udp的connect()
默認udp一個套接字是無連接的,可以向多個地址send
數據,但是一旦connect()
就只能向一個地址發送數據了。
4.1.2.4 常見錯誤碼
EACCES
, EPERM
:用戶試圖在套接字廣播標誌沒有設置的情況下連接廣播地址或由於防火墻策略導致連接失敗。
EADDRINUSE
98:Address already in use(本地地址處於使用狀態)
EAFNOSUPPORT
97:Address family not supported by protocol(參數serv_add中的地址非合法地址)
EAGAIN
:沒有足夠空閑的本地端口。
EALREADY
114:Operation already in progress(套接字為非阻塞套接字,並且原來的連接請求還未完成)
EBADF
77:File descriptor in bad state(非法的文件描述符)
ECONNREFUSED
111:Connection refused(遠程地址並沒有處於監聽狀態)
EFAULT
:指向套接字結構體的地址非法。
EINPROGRESS
115:Operation now in progress(套接字為非阻塞套接字,且連接請求沒有立即完成)
EINTR
:系統調用的執行由於捕獲中斷而中止。
EISCONN
106:Transport endpoint is already connected(已經連接到該套接字)
ENETUNREACH
101:Network is unreachable(網絡不可到達)
ENOTSOCK
88:Socket operation on non-socket(文件描述符不與套接字相關)
ETIMEDOUT
110:Connection timed out(連接超時)
4.1.3 bind()
常用用於服務器綁定ip和端口,進行監聽
一個tcp連接,需要一對套接字地址結構:對端和本端。在上面的connect
中,我們是客戶端想服務端主動發起一個連接請求,指定了對端的套接字結構,但是沒有指定本端的,此時內核會為我們隨機選取端口號(當然有範圍要求)和ip地址(當本端有多個網卡時)。也就是說本端套接字地址結構是隨機的。
但是在服務器端,服務器本端的套接字地址結構需要固定,不然客戶端怎麽連接。因此服務器需要顯式顯式的指明套接字描述符的本端套接字地址結構。
bind()
函數的主要作用是,為套接字描述符綁定本端的套接字地址結構,也就是綁定ip地址和端口。
#incldue <sys/socket.h> int bind(int sockfd, const struct sockaddr *selfaddr, socklen_t len); //成功0,錯誤-1
-
sockfd
套接字描述符 -
sockaddr *selfad
需要強轉,這個套接字地址結構,通常是綁定的本端的信息。客戶端連接本端時候使用的ip,客戶端連接本端使用的端口,也就是
connect()
中的套接字結構信息是一樣的。 -
len
內核復制數據需要的長度。
sockaddr *selfad
各種情況:
-
端口
是一個需要讓別人知道的端口。如果不指定,那麽內核隨機選,那麽你還綁定幹啥?
-
ip地址
ip地址必須是本機的地址(當存在多個網卡,多個ip地址的時候需要綁定),一旦綁定了,那麽只有
connect()
填寫的ip地址是這個ip地址的時候,數據才能被接受,不是的就丟棄了。當該ip不填,是0的時候,那麽所有發送到本機的數據包都能被接受。後續的鏈接如果不設設置REUSEADDR那麽不能綁定成功.
通配地址
當不顯示指明ip地址的時候,一般需要下面的宏,和變量
//ipv4 seraddr.sin_addr.s_addr=htonl(INADDR_ANY); //ipv6 #include <netinet/in.h> ? extern in6addr_any; seraddr.sin6_addr=in6addr_any;
這裏使用htonl()
原因在,在套接字地址結構中的數據都是網絡字節序的。
bind()
通常的錯誤是EADDRINUSE
表示本端的這個套接字已經在使用了,一般是地址.端口不能重用
4.1.4 listen()
上面bind了套接字地址結構以後,還沒有開始監聽啊
#include <sys/socket.h> ? int listen(int sfd, int backlog); //成功0,錯誤-1
listen()
的主要作用:
-
將主動套接字轉化為被動套接字
套接字的狀態從
closed
到listen
-
規定套接字排隊的最大連接個數
-
已完成連接隊列
當次隊列為空的時候,
accept()
休眠 -
未完成連接隊列
正在進行三次握手的連接
-
其實這裏還有另一層意思,當我們給以套接字描述符綁定本端的時候,意味著我們可以讀取這個文件描述符。當對端發來數據以後,我們從listen
的文件描述符中讀取已經連接的文件描述符。監聽套接字是一個只讀套接字
4.1.5 accept()
該函數的作用是返回已完成連接隊列的第一個連接的套接字描述符,如果隊列為空,那麽該函數將被阻塞(如果是阻塞套接字的話)。
#include <sys/socket.h> int accpet(int sfd, struct sockaddr *cliaddr, socklen_t *addrlen); //成功描述符,錯誤-1
-
cliaddr
是將要被內核填充的對端的套接字地址結構。addrlen
也是內核要填充的地址結構的長度。這兩個參數都可以為NULL
如果函數成功返回,那麽返回的是一個套接字描述符,這個描述符關聯了一對套接字地址結構,因此可以當做普通文件描述符來使用。
accpet()
涉及到兩個描述符,一個是sfd
稱為監聽描述符,而函數返回的描述符是已連接套接字描述符,我們使用這個文件描述符與對端進行數據交換。
4.1.6 close()
#include <unistd.h> int close(int fd); //成功0,錯誤-1
這個函數的功能是,將文件描述符的引用計數-1,當為0的時候,則關閉文件描述符,tcp情況下觸發四次揮手操作,或是rst。
在accept
返回的文件描述符應該及時關閉。
4.1.7 get××name()
獲取一個套接字描述符綁定一對套接字地址結構
#include <sys/socket.h> int getsockname(int sfd, struct sockaddr *localaddr, socklen_t *addrlen); int getpeername(int sfd, struct sockaddr *peeraddr, socklen_t *addrlen); //成功0,錯誤-1
這兩個函數是用來讓返回一個套接字描述符綁定的兩個套接字地址結構。
我們可以使用這兩個函數:
-
getsockname
獲取內核為我們選去的ip地址和端口號、協議族 -
getpeername
可以返回對端的套接字地址結構(和accpet
填充的一樣?) -
getpeername
是唯一能夠在accept
以後又調用exec
函數後獲得對端套接字地址結構的函數。返回的已完成連接套接字文件描述符不是
O_EXECLOSE
,因此在exec
以後仍然打開,同時,exec
以後,所有的地址信息不能用了,因此accept
的就不能用了。所以需要getpeername
返回對端套接字地址結構。然後,通過在
exec
的時候傳入文件描述符,或是在exec
之前將文件描述符更改為exec
要執行程序默認的一個文件描述符。傳遞參數(inetd
采用第二種)。
4.1.8 shutdown()
close
終止兩個方向上的數據傳輸
#include <sys/socket.h> int shutdown(int sockfd, int howto); //成功0,錯誤-1
-
howto
表示行為:-
SHUT_RD
關閉連接的讀,也就是套接字本端不再接受數據,緩沖區現有數據被丟棄也不能再使用讀函數對套接字進行操作,對TCP套接字該調用之後接受到的任何數據將被確認然後無聲的丟棄掉。
會超時然後EPOLLERR?應為不會發送FIN.
-
SHUT_WR
關閉連接的寫,也就是本端不再寫數據,緩沖區中現有數據將被發送,然後發送FIN分節。EPOLLIN事件
-
SHUT_RDWR
第一次調用SHUT_RD
,然後再調用SHUT_WR
-
-
sockfd
文件描述符
使用close中止一個連接,但它只是減少描述符的引用計數,並不直接關閉連接,只有當描述符的參考數為0時才關閉連接。
shutdown可直接關閉描述符,不考慮描述符的參考數,可選擇中止一個方向的連接。
第4章 基本tcp套接字編程