五.TCP原理
5.1 TCP Socket中的IO緩衝
TCP Socket的資料無邊界,即write
和read
次數並不對應,多次傳送的資料,可以透過read一次完成讀取,一次傳送的資料,也可以每次接收一部分,多次完成讀取。
這主要是透過IO緩衝完成的。
呼叫write函式時,資料並未傳送,而是移到輸出緩衝
,在適當的時候傳向對方的輸入緩衝
。
另一方可呼叫read函式從輸入緩衝
讀取資料。這些IO緩衝特性如下:
- IO緩衝在每個TCP Socket中單獨存在
- IO緩衝在建立Socket時自動生成
- 即使關閉Socket也會繼續傳送輸出緩衝中遺留的資料
- 關閉Socket將丟失輸入緩衝中的資料
如果一次傳送很大的資料,對方的輸入緩衝放不下怎麼辦?
TCP使用滑動視窗,如果放不下,就停止傳送了。
write函式返回的時間點
write函式並不是在完成向對方主機的資料傳輸後才返回,而是在資料移到輸出緩衝時。剩下的事情由TCP完成,TCP會完成對輸出緩衝資料的傳輸,
因此write函式是在資料傳輸到輸出緩衝時返回。
5.2 TCP連線
SEQ和ACK都以位元組為單位,ACK指的是想要接收的下一個資料包的首位元組編號
所以如果SEQ = 1200,傳送100位元組資料,那麼下一個ACK就是1301
5.3 傳送資料
5.4 斷開連線
A通知B要斷開連線
B通知A:知道了,請稍等,我做好掃尾工作
B準備好後通知A:可以關閉連線了
A通知B:好的
為什麼ACK兩次都是5001?
因為B第一次傳送的ACK = 5001沒有收到資料,所以資料編號不變
六、基於UDP的C/S
- UDP無需建立連線,就可以傳送
- UDP的server和client端都只需一個Socket,而TCP中,服務端需要為每個客戶端建立一個Socket
- 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個階段
- 在UDP Socket中註冊目標IP和Port
- 傳輸資料
- 刪除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
之後,就可以使用sendto
和recvfrom
進行收發資料,也可使用read
和wriet
通訊
write(sock, msg, strlen(msg));
read(sock, msg, sizeof(msg) - 1);
printf("Receive From Server: %s\n", msg);
close(sock);
七、半斷開
- socket由兩個流組成:
輸入流
和輸出流
- 可以精細化操作,單獨關閉輸入或輸出流
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 關閉流的總結
-
呼叫
close()
close(socket)
時,會向對方傳送一個RST報文
,解釋如下RST(Reset)標誌位用於中斷(reset)TCP連線。當TCP報文段中的RST標誌位被設定為1時,它表示傳送方或接收方希望立即中止連線,並且不希望繼續進行TCP通訊。
-
半關閉
-
關閉輸入流時,不會傳送任何報文,但如果之後收到對方發來的資料,將不會接收,同時回覆
RST報文
。關閉輸入流之後,仍然可以讀取
輸入緩衝
中的資料,因此可以讀取關閉之前接收到的資料。 -
關閉輸出流時,會傳送
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);
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選項
- 協議是分層的,
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
在執行程式時候,有時候會出現bind() error
錯誤,這是因為埠處在TIME_WAIT
狀態,因此無法被使用,一般等待幾分鐘即可
- 在TCP的四次揮手過程中,
TIME_WAIT
狀態由主動發起關閉的一方承擔。 - 之所以有這個狀態,是擔心最後一個報文丟失,接收方重傳,此時發起方已經關閉,導致無法回覆的情況。
可以透過設定SO_REUSEADDR
為1
,避免這種情況
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演算法