Preview
基於上一篇部落格,本文將繼續展開TCP面向連線的,客戶端以及服務端各自需要進行的操作,我們按照真實TCP連線的順序,分別闡述客戶端socket(), connect()以及服務端socket(), bind(), listen(), accept()建立連線的過程。連線建立之後,闡述send(), recv()的具體細節。
Create Socket
UNIX系統萬物皆檔案的思想,引入了重要的檔案描述符概念,詳情可以閱讀CS:APP的UNIX I/O章節。簡單類比,可以將檔案描述符看作一個指標陣列的index,指標陣列指向的內容與檔案相關。
在socket程式設計中,有兩種方式建立新的套接字並獲取對應的檔案描述符,socket()以及accept(),本章節主要介紹socket()
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
可以理解,創立一個套接字,必須要獲得協議相關內容,例如指明TCP/IP協議。
本部落格主要針對TCP,所以以此陳述。
相應的,domain就代表連線使用的是IPv4還是IPv6。那麼type就對應的是SOCK_STREAM。protocol就需要是知名是tcp還是UDP(其實type等同於TCP/UDP不太精準,只是說TCP是基於SOCK_STREAM),這個可以利用getprotobyname()函式獲取。
事實上,socket的三個引數我們是利用getaddrinfo()獲取的關於addrinfo連結串列寫入的(真就工具人唄)
- domain: ai_family
- type: ai_socktype
- protocol: ai_protocol
關於domain,這裡又有一段歷史...
domain
isPF_INET
orPF_INET6
This
PF_INET
thing is a close relative of theAF_INET
that you can use when initializing thesin_family
field in yourstruct sockaddr_in
. In fact, they’re so closely related that they actually have the same value, and many programmers will callsocket()
and passAF_INET
as the first argument instead ofPF_INET
. Now, get some milk and cookies, because it’s time for a story. Once upon a time, a long time ago, it was thought that maybe an address family (what the “AF” in “AF_INET
” stands for) might support several protocols that were referred to by their protocol family (what the “PF” in “PF_INET
” stands for). That didn’t happen. And they all lived happily ever after, The End. So the most correct thing to do is to useAF_INET
in yourstruct sockaddr_in
andPF_INET
in your call tosocket()
.
Client
先說簡單而無腦的客戶端,TCP的3次握手總得有人先握手,connect()便是開啟握手過程的函式
connect()
開始和人打招呼,得先知道別人在哪,對應網際網路就是套接字地址,利用上一篇部落格的內容就可以輕鬆愉快的獲得了。
深入這些之後就發現,逐步和計網的課正在結合起來,getaddrinfo()有些類似於DNS域名解析,connect()就類似於開始握手。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
函式的引數是好理解的,你需告知系統,是哪個套接字,去找誰,開啟連線,此處需要addrlen(),即sizeof( *serv_addr)
,應該是函式內部具有更多細節。
這裡就可以給出結合socket(), connect()客戶端發起連線的一系列準備工作了
struct addrinfo hints, *res;
int sockfd;
memset(&hints, 0, sizeof(hints));
hints.ai_family= AF_UNSPEC;
hints.ai_socketype= SOCK_STREAM;
getaddrinfo("www.example.com", "3490", &hints, &res);
sockfd= socket(res->ai_family, res->ai_socktype, res->ai_protocol);
connect(sockfd, res->ai_addr, res->ai_addrlen);
Server
伺服器的活就多了,因為需要考慮讓很多人來連線,所以需要固定埠號(bind()), 預設套接字開啟是用來找別人的(CS:APP話來說,主動套接字),需要改編為可以監聽別人進來的資料(listen()),接受以後,計網知識對應的,需開啟連線套接字(accept())。
bind()
在前面客戶端部分未陳述,事實上,核心對於套接字的埠開始是隨便分的,排除已經使用的,以及周知埠隨機分配。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
很簡單的引數設定,和哪個套接字bind(), 把這個套接字bind()上的地址,還有也許出於函式設定的addrlen: sizeof(*my_addr)
先看看老派的做法:
int sockfd;
struct sockaddr_in my_addr;
sockfd= socket(PF_INET, SOCK_STREAM, 0);
my_addr.sin_family= AF_INET;
inet_pton(AF_INET, "10.12.110.57", &(my_addr.sin_addr));
// actually older way is my_addr.sin_addr.s_addr=inet_addr("10.12.110.57");
// or my_addr.sin_addr.s_addr= INADDR_ANY;
my_addr.sin_port= htons(MYPORT);
memset(my_addr.sin_zero, 0, sizeof(my_addr))
還是換工具人上場吧
struct addrinfo hints, *res;
int sockfd;
memset(&hints, 0, sizeof(hints));
hints.ai_family= AF_UNSPEC;
hints.ai_socktype= SOCK_STREAM;
hints.ai_flags= AI_PASSIVE;
getaddrinfo(NULL, "3490", &hints, &res);
sockfd= socket(res->ai_family, res->ai_socktype, res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);
這就將上一篇部落格提到的模板連線起來了。
listen()
話不多,上定義
int listen(int sockfd, int backlog)
指定好兩件事,讓誰監聽,最多能處理幾個,這就分別對應了sockfd, backlog。
accept()
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
這裡就是涉及到計網的知識了,TCP面向連線時伺服器端,是專門利用一個套接字監聽,稱為監聽套接字,再利用fork()(CS:APP異常章節),建立了新的連線套接字來和客戶端互動,這樣做也好理解,例如一個web應用,總不可能全世界每時每刻就讓一個人連線他。
顧名思義,猜測這個函式應該還有傳送回去ACK的功能
accept()引數是這樣設定的,從哪個監聽套接字收到了連線請求?我總得知道這個連線是哪來的?以及老生常談的addrlen
Easy enough.
addr
will usually be a pointer to a localstruct sockaddr_storage
. This is where the information about the incoming connection will go (and with it you can determine which host is calling you from which port).addrlen
is a local integer variable that should be set tosizeof(struct sockaddr_storage)
before its address is passed toaccept()
.accept()
will not put more than that many bytes intoaddr
. If it puts fewer in, it’ll change the value ofaddrlen
to reflect that.
這裡就有細節需要注意,他是記錄到sockaddr_storage結構裡,前面介紹過,這樣IPv4, IPv6通吃,addrlen設定也很有意思,相當於是一個放入地址上限的意思,但是放少了,又會把他改掉。
Communication
前面連線沒問題,就開始各種交流吧
這兩個函式針對的是stream socket,就是設定了SOCK_STREAM的。
send()
int send(int sockfd, const void *msg, int len, int flags);
你需要通過哪個套接字幫你傳送訊息(你把待發資訊交給他處理)(sockfd)?處理的資訊是啥(msg)?發多少(len)?傳送姿勢是啥(通常為0,遇事不決man一下)?
recv()
int recv(int sockfd, void *buf, int len, int flags);
你想從哪個套接字接受發過來的資料(sockfd)?放到哪(buf)?最多能接受多少(len,注意這裡和send()是不同的,這裡是最多 可以接受多少資訊)?接受姿勢是啥(通常也是0)?
Conclusion
至此,你已經可以寫一個簡單的類似於OICQ之類的玩意了,關於TCP的socket()程式設計簡單介紹就結束了,隨後會加上示例程式碼。