TCP服務器端:
int socket(int domain , int type , int protocol) domain(協議族):常用的協議族便是IPV4(PF_INET), IPV6(PF_INET6),本地通信協議的UNIX族(PF_LOCAL) type:數據傳輸類型;典型數據傳輸類型:面向連接的套接字(SOCK_STREAM),面向消息的套接字(SOCK_DGRAM) protocal:具體協議; 返回套接字文件描述符,在linux中,不區分套接字和文件,統一用文件描述符來描述;
TCP與UDP的區別:
- TCP是面向連接,UDP是無連接的傳輸
- TCP保證了數據傳輸的正確和有序,而UDP不保證
- TCP數據傳輸是無邊界的,也就是流模式(待查),UDP傳輸是有邊界的,采用數據報模式(待查)
- TCP需要更多的系統資源
int bind(int sockfd , const struct sockaddr* servaddr , socklen_t addrlen); 給套接字分配IP地址和端口號,將套接字和相應的IP地址和端口號綁定,失敗返回-1;
當沒有給套接字綁定IP地址和端口號(端口號指定為0)時,bind函數被調用時會分配一個臨時端口號與套接字進行綁定,但是IP地址會當TCP連接建立(客戶端目的IP地址)或者在此套接字上發送UDP數據報時才會綁定IP地址;當IP地址指定為通配地址(INADDR_ANY)時,由內核來選定IP地址)
int listen(int fd , int backlog) 將套接字變為可接受連接狀態;內核為監聽套接字維護兩個隊列,一個是未完成連接隊列,該隊列中的每一項為客戶端發送的SYN字節,另一個是已完成連接隊列,存儲已完成連接的客戶端套接字,當accept調用時,就從該隊列的隊頭返回,當該隊列為空的時候,進程進入睡眠,等待被喚醒
int accept(int sockfd , struct sockaddr* addr , socklen_t* addrlen) ; 接受連接,返回一個全新的套接字,稱為“已連接套接字”,由內核創建的一個全新的套接字,自動完成與對應的客戶端套接字的連接(三路握手),由這個全新的套接字與客戶端套接字進行通信
TCP客戶端:
int connect(int sockfd , struct sockaddr* servaddr, socklen_t addrlen) connect函數完成兩件事,如果套接字沒有被綁定IP地址和端口號,內核會分配一個臨時端口號,並與套接字進行綁定。如果是TCP套接字則會向服務器發起連接,進行三路握手,連接成功或失敗才會返回;
基於TCP的服務器端/客戶端函數調用過程:

TCP三路握手和四次揮手:

TCP為什么要三次握手?
因為客戶端和服務器之間要互相同步各自的初始信號,所以每一個SYN都需要一個ACK,因為服務器端的SYN和ACK可以合並發送,所以需要三次;
TCP為什么要四次揮手?
因為客戶端和服務器之間是全雙工連接,每一個FIN都需要一個ACK。因為服務器端的FIN和ACK不能合並的原因是在某些情況下,客戶端不再向服務器端發送數據后,服務器端可能還要向客戶端發送數據,所以不能合並,所以需要四次。
為什么執行主動關閉的那端需要進入TIME_WAIT狀態?
首先ACK數據包可能在網絡中丟失,所以被動關閉那端可能需要重傳FIN,這就需要主動關閉那端維持狀態准備重傳ACK;另一個原因是網絡中可能存在舊連接的一些迷路的重復分組,處於TIME_WAIT狀態下的連接不能建立新的化身,TIME_WAIT狀態會持續2個MSL時間,足夠使得舊連接的重復分組消失在網絡中,避免新連接出現舊連接的重復分組;
TCP套接字的I/O緩沖
- 輸入緩沖和輸出緩沖單獨存在
- I/O緩沖在創建套接字的時候自動生成
- 關閉套接字后仍會繼續傳遞輸出緩沖中的數據
- 關閉套接字后會丟失輸入緩沖中的數據
如何優雅的斷開套接字連接(半關閉數據流):
int shutdown(int sock , int howto); howto:SHUT_RD(斷開輸入流) , SHUT_WR(斷開輸出流),SHUT_RDWR(斷開輸入輸出流);
為什么要半關閉數據流?
當服務器文件傳輸結束但客戶端還有數據傳輸的時候,如果直接調用close關閉套接字表示文件傳輸結束,那么也無法接收客戶端傳來的數據。在此種應用場景之下,可以調用shutdown函數關閉輸出流表示文件傳輸結束,但是此時輸入流並沒有關閉,可以繼續接收數據傳輸;
tcp_server.c #include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/socket.h> #define BUF_SIZE 1024 void error_handling(char* m) ; int main(int argc , char* argv[]) { int listen_sock , connd_sock , readlen; struct sockaddr_in serv_addr , clnt_addr ; socklen_t addr_len; if(argc != 2) error_handling("argc error") ; listen_sock = socket(PF_INET , SOCK_STREAM , 0) ; if(listen_sock == -1) error_handlind("socket error") ; serv_addr.sin_family = AF_INET ; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY) ; serv_addr.sin_port = htons(atoi(argv[1])); if(bind(listen_sock , (struct sockaddr*)&serv_addr , sizeof(serv_addr)) == -1) error_handling("bind error") ; if(listen(listen_sock , 5) == -1) error_handling("error listen") ; addr_len = sizeof(clnt_addr) ; for(int i = 0 ; i < 5 ; i++) { connd_sock = accept(listen_sock , (struct sockaddr*)&clnt_addr , &addr_len) ; if(connd_sock == -1) error_handling("error accept") ; while(readlen = read(connd_sock , message , BUFSIZE) > 0) write(connd_sock , message , readlen) ; close(connd_sock) ; } close(listen_sock); return 0 ; }
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<arpa/inet.h> #include<sys/socket.h> #define BUFSIZE 1024 void error_handling(char* m); int main(int argc , char* argv[]) { int clnt_sock; struct sockaddr_in serv_addr; if (argc != 3) error_handling("error argc"); clnt_sock = socket(PF_INET, SOCK_STREAM, 0); if (clnt_sock == -1) error_handling("error socket"); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); if (connect(clnt_sock, (struct sock_addr*) & serv_addr, sizeof(serv_addr)) == -1) error_handling("connect error"); char message[BUFSIZE]; int sendlen , recvlen , readcnt; while (1) { fputs("input message(q to quit): ", stdout); fgets(message, BUFSIZE, stdin); if (message == "q\n" || message == "Q\n") break; sendlen = write(clnt_sock, message, sizeof(message)); recvlen = 0; while (recvcnt = read(clnt_sock, message + recvlen, BUF_SIZE - 1) > 0) { recvlen += recvcnt; if (recvlen >= sendlen) break; } message[recvlen] = 0; fputs(message, stdout); } close(clnt_sock); return 0; }
基於UDP的數據傳輸:
- 雙方通信只各自需要一個套接字
- 通信之前不需要建立連接,通信結束后也不需要斷開連接;
- 通信數據存在數據邊界
UDP的數據I/O函數:
ssize_t sendto(int sockfd , void *buff , size_t ntypes , int flags , struct sockaddr* to , socklen_t addrlen) ; ssize_t recvfrom(int sockfd , void *buff , size_t ntypes , int flags , struct sockaddr* from , socklen_t* addrlen) ;
如果在調用I/O函數之前沒有給套接字分配IP地址和端口號,那么在第一次調用sendto函數會自動向套接字分配IP地址和端口號;
sendto函數調用過程:
- 向套接字注冊目標地址
- 數據傳輸
- 套接字清除目標地址
如果要多次向同一個目標地址傳輸數據,可以向套接字注冊目標地址(調用connect函數,如何清除呢?),變成已連接套接字;
阻塞I/O和非阻塞I/O:
