5~9章

INnoVation-V2發表於2024-07-03

五.TCP原理

5.1 TCP Socket中的IO緩衝

image-20221129174333338

TCP Socket的資料無邊界,即writeread次數並不對應,多次傳送的資料,可以透過read一次完成讀取,一次傳送的資料,也可以每次接收一部分,多次完成讀取。

這主要是透過IO緩衝完成的。

呼叫write函式時,資料並未傳送,而是移到輸出緩衝,在適當的時候傳向對方的輸入緩衝

另一方可呼叫read函式從輸入緩衝讀取資料。這些IO緩衝特性如下:

  • IO緩衝在每個TCP Socket中單獨存在
  • IO緩衝在建立Socket時自動生成
  • 即使關閉Socket也會繼續傳送輸出緩衝中遺留的資料
  • 關閉Socket將丟失輸入緩衝中的資料

如果一次傳送很大的資料,對方的輸入緩衝放不下怎麼辦?

TCP使用滑動視窗,如果放不下,就停止傳送了。

write函式返回的時間點

write函式並不是在完成向對方主機的資料傳輸後才返回,而是在資料移到輸出緩衝時。剩下的事情由TCP完成,TCP會完成對輸出緩衝資料的傳輸,

因此write函式是在資料傳輸到輸出緩衝時返回。

5.2 TCP連線

image-20221129175553595

SEQ和ACK都以位元組為單位,ACK指的是想要接收的下一個資料包的首位元組編號

所以如果SEQ = 1200,傳送100位元組資料,那麼下一個ACK就是1301

5.3 傳送資料

image-20221129175818774

5.4 斷開連線

image-20221129175857184

A通知B要斷開連線

B通知A:知道了,請稍等,我做好掃尾工作

B準備好後通知A:可以關閉連線了

A通知B:好的

為什麼ACK兩次都是5001?

因為B第一次傳送的ACK = 5001沒有收到資料,所以資料編號不變

六、基於UDP的C/S

  1. UDP無需建立連線,就可以傳送
  2. UDP的server和client端都只需一個Socket,而TCP中,服務端需要為每個客戶端建立一個Socket
  3. UDP的send和recv的次數要一一對應

6.1 UDP相關的函式

#include <sys/socket.h>

/*
	sock: 用於傳輸資料的UDP Socket
	buf:  待傳輸資料所在地址
	nbytes: 待傳輸資料長度,單位為B
	flags:  可選項引數
	to:		  目標地址
	addrlen: to結構體的長度
*/
//成功返回傳送位元組數,失敗-1
ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);



/*
	sock: 用於傳輸資料的UDP Socket
	buf:  待傳輸資料所在地址
	nbytes: 待傳輸資料長度,單位為B
	flags:  可選項引數
	from:		存有傳送方地址資訊的結構體
	addrlen: from結構體的長度
*/
//成功返回接收位元組數,失敗-1
ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *from, socklen_t addrlen )

6.2 對UDP Socket進行Connect

在UDP中,透過sendto進行資料傳輸的過程大致分為3個階段

  1. 在UDP Socket中註冊目標IP和Port
  2. 傳輸資料
  3. 刪除UDP Socket中註冊的地址資訊

因此只需一個Socket,UDP就能向多個Socket傳送資料,這種Socket被稱作未連線Socket。

但是當遇到如下情況時,情況顯得不太合理

向同一目標傳送5個資料包,且資料包不能合併

此時就需要重複上述階段,第1、3步需要重複執行5次,完全是浪費資源和時間,這種情況下,可以對Socket進行Connect,Connect的UDP Socket只需要註冊一次目標資訊,之後就可以不停地傳送。

sock = socket(PF_INET, SOCK_DGRAM, 0);
struct sockaddr_in server_addr{};
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
server_addr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr));

和TCP建立過程一致,但是建立Socket的第二個引數是SOCK_DGRAM,說明建立的是UDP

之後,就可以使用sendtorecvfrom進行收發資料,也可使用readwriet通訊

write(sock, msg, strlen(msg));
read(sock, msg, sizeof(msg) - 1);
printf("Receive From Server: %s\n", msg);
close(sock);

七、半斷開

  1. socket由兩個流組成: 輸入流輸出流
  2. 可以精細化操作,單獨關閉輸入或輸出流

7.1 半關閉

#include<sys/socket.h>

/* 
	成功返回0, 失敗-1
	sock: 要斷開的套接字
	howto: 要斷開哪條流
		SHUT_RD:  斷開輸入流
		SHUT_WR:  斷開輸出流
		SHUT_RDWR:斷開IO流
*/
int shutdown(int sock, int howto);

7.2 關閉流的總結

  1. 呼叫close()

    close(socket)時,會向對方傳送一個RST報文,解釋如下

    RST(Reset)標誌位用於中斷(reset)TCP連線。當TCP報文段中的RST標誌位被設定為1時,它表示傳送方或接收方希望立即中止連線,並且不希望繼續進行TCP通訊。
    
  2. 半關閉

    1. 關閉輸入流時,不會傳送任何報文,但如果之後收到對方發來的資料,將不會接收,同時回覆RST報文

      關閉輸入流之後,仍然可以讀取輸入緩衝中的資料,因此可以讀取關閉之前接收到的資料。

    2. 關閉輸出流時,會傳送FIN報文

八、DNS

因為IP經常變換,而域名相對穩定,因此使用DNS動態獲取IP地址

8.1 利用域名獲取IP地址

#include<netdb.h>
struct hostent {
	char	*h_name;			// 官方域名
	char	**h_aliases;	// alias list, 多個別名指向同一個official name
	int	h_addrtype;			// 支援的通訊型別IPV4或IPV6
	int	h_length;				// IP地址長度
	char	**h_addr_list;// IP地址列表,一個域名可能有多個IP地址
};

struct hostent* gethostbyname(const char* hostname);
image-20221130161008112

8.2 利用IP地址獲取域名

#include<netdb.h>

/*
	失敗返回NULL指標
	1.addr: 包含地址資訊的in_addr結構體指標。為了同時傳遞IPv4地址之外的其他資訊,該變數的型別宣告為char指標
	2.len: 向第一個引數傳遞的地址資訊的位元組數,1Pv4時為4,IPv6時為16。
	3.family: 傳遞地址族資訊,1PV4時為AF_INET,IPv6時為AF_INET6。
*/
struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);

九.socket修改引數

9.1 常見Socket選項

image-20230715190629741 image-20230715190641508
  • 協議是分層的,IPPROTO_IP是IP層相關選項,IPPROTO_TCP是TCP相關選項,SOL_SOCKET是socket的通用選項
  • 大多數選項既可以讀,也可以修改,但是有的選項是隻讀的

9.2 檢視、修改Socket引數

getsockopt()

#include<sys/socket.h>
/*
	成功0,失敗-1
	1.sock: socket
	2.level: 要檢視的選項所在協議層
	3.optname: 要檢視的選項名
	4.optval: 儲存結果的地址值
	5.optlen: optval的大小。儲存第四個引數返回的選項資訊的位元組數
*/
int getsockopt(int sock, int level, int optname, void* optval, socklen_t* optlen);

setsockopt()

#include<sys/socket.h>

/*
	成功0,失敗-1
	1.sock: socket
	2.level: 要修改的選項所在協議層
	3.optname: 要修改的選項名
	4.optval: 儲存選項資訊的地址值
	5.optlen: 第四個引數的位元組數
*/
int setsockopt(int sock, int level, int optname, const void* optval, socklen_t* optlen);

9.3 修改TCP緩衝大小

1.SO_SNDBUF: 輸入緩衝大小
2.SO_RCVBUF: 輸出緩衝大小
// 1.讀取輸入緩衝大小
sock = socket(PF_INET, SOCK_STREAM, 0);
int snd_buf;
socklen_t len = sizeof(snd_buf);
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);

// 2.修改輸出緩衝大小
sock = socket(PF_INET, SOCK_STREAM, 0);
int snd_buf = 1024*3;
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));

9.4 SO_REUSEADDR

image-20230715192413214

在執行程式時候,有時候會出現bind() error錯誤,這是因為埠處在TIME_WAIT狀態,因此無法被使用,一般等待幾分鐘即可

  1. 在TCP的四次揮手過程中,TIME_WAIT狀態由主動發起關閉的一方承擔。
  2. 之所以有這個狀態,是擔心最後一個報文丟失,接收方重傳,此時發起方已經關閉,導致無法回覆的情況。

可以透過設定SO_REUSEADDR1,避免這種情況

int option = 1;
setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, sizeof(option));

9.5 TCP_NODELAY

首先簡單介紹naggle演算法,Nagle演算法是一個流量控制演算法,用於最佳化TCP連線中的資料傳輸效率。它透過延遲傳送小資料包,將多個小資料包合併成一個大的資料包進行傳送,以減少網路中的小資料包數量,提高傳輸效率。

naggle演算法簡單流程如下

1.當呼叫write傳送資料時,資料被儲存在傳送緩衝中,不會立即傳送。
2.當傳送緩衝區達到一定大小(通常是最大報文段長度MSS)時,觸發傳送
3.或者200ms內沒有新的資料寫入時,也會傳送,如果在等待時間內有新資料寫入,則會重置等待
4.在等待時收到的之前訊息的ACK,則會將多個ACK合併為一個ACK到待傳送的資料包中,以減少ACK的數量

總而言之,Nagle演算法可以提高網路傳輸效率,但可能會導致一定的延遲,因為他不會立刻傳送資料,因此可以透過設定TCP_NODELAY = 1關掉naggle演算法