Linux網路程式設計(2)

IdiotNe發表於2020-04-26

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 is PF_INET or PF_INET6

This PF_INET thing is a close relative of the AF_INET that you can use when initializing the sin_family field in your struct sockaddr_in. In fact, they’re so closely related that they actually have the same value, and many programmers will call socket() and pass AF_INET as the first argument instead of PF_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 use AF_INET in your struct sockaddr_in and PF_INET in your call to socket().

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 local struct 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 to sizeof(struct sockaddr_storage) before its address is passed to accept(). accept() will not put more than that many bytes into addr. If it puts fewer in, it’ll change the value of addrlen 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()程式設計簡單介紹就結束了,隨後會加上示例程式碼。

相關文章