【Linux网络编程】TCP
概述
TCP(Transmission Control Protocol 傳輸控制協議)是一種面向連接的、可靠的、基于字節流的傳輸層通信協議。
TCP 具有以下特點:
1)電話系統服務模式的抽象
2)每一次完整的數據傳輸都要經過建立連接、使用連接、終止連接的過程
3)可靠、出錯重傳、且每收到一個數據都要給出相應的確認,保證數據傳輸的可靠性
TCP 編程的 C/S 架構
基于 TCP 的網絡編程開發分為服務器端和客戶端兩部分,常見的核心步驟和流程如下:
TCP 客戶端編程
對于 TCP 客戶端編程流程,有點類似于打電話過程:找個可以通話的手機(socket()?)?->?撥通對方號碼并確定對方是自己要找的人(?connect()?)?->?主動聊天(?send()?或?write()?)->?或者,接收對方的回話(?recv()?或?read()?)->?通信結束后,雙方說再見掛電話(close()?)。
所需頭文件:#include <sys/socket.h>
int socket(int family,int type,int protocol);
功能: 創建一個用于網絡通信的 socket套接字(描述符),詳細用法,請看《套接字的介紹》
參數: family:本示例寫?AF_INET,代表 IPv4 type:本示例寫?SOCK_STREAM,代表 TCP 數據流 protocol:這里寫 0,設為 0 表示使用默認協議
返回值: 成功:套接字 失敗 < 0?
int connect( int sockfd,?const struct sockaddr *addr,?socklen_t len );
功能:
主動跟服務器建立連接,有點類似于,我們給別人電話,主動撥對方的電話號碼,具體是怎么一個過程,請《connect()、listen()和accept()三者之間的關系》。
參數:
sockfd:socket()返回的套接字
addr:連接的服務器地址結構
len:地址結構體長度
返回值:
成功:0 ? ?
失敗:-1
connect() 函數相當于撥號碼,只有撥通號碼并且確定對方是自己要找的人(三次握手)才能進行下一步的通信。
ssize_t send(int sockfd,?const void* buf,?size_t nbytes,?int flags);
功能:
發送數據,最后一個參數為 0 時,可以用 write() 替代(?send 等同于 write?)。注意:不能用 TCP 協議發送 0 長度的數據包。假如,數據沒有發送成功,內核會自動重發。
參數:
sockfd: 已建立連接的套接字
buf:?發送數據的地址
nbytes: 發送緩數據的大小(以字節為單位)
flags:?套接字標志(常為 0)
返回值:
成功:成功發送的字節數
失敗 < 0
虛擬機中Redhat5.5的 TCP 客戶端程序代碼:
#include <stdio.h> #include <stdlib.h> #include <string.h>#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>#define PORT 10086 #define SIZE 128/***服務端:* a. 創建一個套字* b. 初始化結構體* c. 綁定* d. 監聽 設置監聽隊列的大小* e. 接受客戶端的連接* f. 讀寫操作**/int main(void) {int sockfd = 0;int newfd = 0;int ret = -1;char buf[SIZE];struct sockaddr_in sockaddr;struct sockaddr_in fromaddr;socklen_t len = sizeof(fromaddr);//a. 創建套接子sockfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == sockfd){perror("socket"); goto err0;}//b. 初始化結構體memset(&sockaddr, 0, sizeof(sockaddr));sockaddr.sin_family = AF_INET; //ipv4sockaddr.sin_port = htons(PORT);sockaddr.sin_addr.s_addr = inet_addr("172.16.1.88");//綁定ret = bind(sockfd, (void*)&sockaddr, sizeof(sockaddr));if (-1 == ret){perror("bind"); goto err1;}//監聽ret = listen(sockfd, 10);if (-1 == ret){perror("listen"); goto err1;}printf("server is waiting the client incomming....\n");//阻塞 接受客戶端連接newfd = accept(sockfd, (void*)&fromaddr, &len);if (-1 == newfd){perror("accept"); goto err1;}//客戶端的信息printf("from client ip: %s port: %d\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port));//讀寫操作while(1){memset(buf, 0, SIZE); //ret = write(newfd, "hello world", 11); ret = send(newfd, "hello world", 11, 0);if (ret <= 0)break;printf("send %d bytes\n", ret);sleep(1);}close(sockfd);return 0; err1:close(sockfd); err0:return -1; }
運行結果如下:
對于客戶端,也是可以接收數據,前提為,客戶端先給服務器發送數據。
ssize_t recv(int sockfd,?void *buf, ?size_t nbytes,?int flags);
功能:
接收網絡數據,默認的情況下,如果沒有接收到數據,這個函數會阻塞,直到有數據到來。
參數:
sockfd:套接字
buf:接收網絡數據的緩沖區的地址
nbytes:接收緩沖區的大小(以字節為單位)
flags:套接字標志(常為 0 )
返回值:
成功:成功接收的字節數
失敗 < 0
測試代碼如下:
#include <stdio.h> #include <stdlib.h> #include <string.h>#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>#define PORT 10086 #define SIZE 128/***客戶端:* a. 創建一個套接子* b. 初始化結構體* e. 鏈接服務器* f. 讀寫操作**/int main(void) {int sockfd = 0;int newfd = 0;int ret = -1;char buf[SIZE];struct sockaddr_in sockaddr;socklen_t len = sizeof(sockaddr);//a. 創建套接子sockfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == sockfd){perror("socket"); goto err0;}//b. 初始化結構體 使用服務器相關信息memset(&sockaddr, 0, sizeof(sockaddr));sockaddr.sin_family = AF_INET; //ipv4sockaddr.sin_port = htons(PORT);sockaddr.sin_addr.s_addr = inet_addr("172.16.1.88");//c. 連接服務器ret = connect(sockfd, (void*)&sockaddr, len);if (-1 == ret){perror("connect"); goto err1;}printf("connect to server successfully....\n");//讀寫操作while(1){memset(buf, 0, SIZE); //ret = read(sockfd, buf, SIZE);ret = recv(sockfd, buf, SIZE, 0);if (ret <= 0)break;buf[ret] = 0;printf("from server: %s\n", buf);}close(sockfd);return 0; err1:close(sockfd); err0:return -1; }
運行結果如下:
TCP 服務器編程
做為 TCP 服務器需要具備的條件呢?
- 具備一個可以確知的地址( bind() ):相當于我們要明確知道移動客服的號碼,才能給他們電話;
- 讓操作系統知道是一個服務器,而不是客戶端( listen() ):相當于移動的客服,他們主要的職責是被動接聽用戶電話,而不是主動打電話騷擾用戶;
- 等待連接的到來( accept() ):移動客服時刻等待著,來一個客戶接聽一個。
接收端使用 bind() 函數,來完成地址結構與socket 套接字的綁定,這樣 ip、port 就固定了,發送端即可發送數據給有明確地址( ip+port ) 的接收端。
對于 TCP 服務器編程流程,有點類似于接電話過程:找個可以通話的手機(socket()?)?->?插上電話卡固定一個號碼(?bind()?)?->?職責為被動接聽,給手機設置一個鈴聲來監聽是否有來電(?listen()?)?->?有來電,確定雙方的關系后,才真正接通不掛電話(?accept()?)?->?接聽對方的訴說(?recv()?)?->?適當給些回話(?send()?)->?通信結束后,雙方說再見掛電話(?close()?)。
int bind( int sockfd,?const struct sockaddr *myaddr,socklen_t addrlen );
功能:
將本地協議地址與 sockfd 綁定,這樣 ip、port 就固定了
參數:
sockfd:socket 套接字
myaddr: 指向特定協議的地址結構指針
addrlen:該地址結構的長度
返回值:
成功:返回 0
失敗:-1
使用實例如下:
// 本地網絡地址 struct sockaddr_in my_addr; bzero(&my_addr, sizeof(my_addr)); // 清空結構體內容 my_addr.sin_family = AF_INET; // ipv4 my_addr.sin_port = htons(port); // 端口轉換 my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 綁定網卡所有ip地址,INADDR_ANY為通配地址,值為0printf("Binding server to port %d\n", port); int err_log; err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr)); // 綁定 if(err_log != 0) {perror("bind");close(sockfd); exit(-1); }
int listen(int sockfd, int backlog);
功能:
將套接字由主動修改為被動,使操作系統為該套接字設置一個連接隊列,用來記錄所有連接到該套接字的連接。更詳細說明,請看《connect()、listen()和accept()三者的關系》。
參數:
sockfd: socket監聽套接字
backlog:連接隊列的長度
返回值:
成功:返回0
失敗:其他
int accept( ?int sockfd,?struct sockaddr *cliaddr,?socklen_t *addrlen );
功能:
從已連接隊列中取出一個已經建立的連接,如果沒有任何連接可用,則進入睡眠等待(阻塞)。更詳細說明,請看《connect()、listen()和accept()三者的關系》。
參數:
sockfd: socket監聽套接字
cliaddr: 用于存放客戶端套接字地址結構
addrlen:套接字地址結構體長度的地址
返回值:
成功:已連接套接字。注意:返回的是一個已連接套接字,這個套接字代表當前這個連接
失敗:< 0
Redhat 中的服務器代碼如下:
#include <stdio.h> #include <stdlib.h> #include <string.h>#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>#define PORT 10086 #define SIZE 128/***服務端:* a. 創建一個套字* b. 初始化結構體* c. 綁定* d. 監聽 設置監聽隊列的大小* e. 接受客戶端的連接* f. 讀寫操作**/int main(void) {int sockfd = 0;int newfd = 0;int ret = -1;char buf[SIZE];struct sockaddr_in sockaddr;struct sockaddr_in fromaddr;socklen_t len = sizeof(fromaddr);//a. 創建套接子sockfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == sockfd){perror("socket"); goto err0;}//b. 初始化結構體memset(&sockaddr, 0, sizeof(sockaddr));sockaddr.sin_family = AF_INET; //ipv4sockaddr.sin_port = htons(PORT);sockaddr.sin_addr.s_addr = inet_addr("172.16.1.88");//綁定ret = bind(sockfd, (void*)&sockaddr, sizeof(sockaddr));if (-1 == ret){perror("bind"); goto err1;}//監聽ret = listen(sockfd, 10);if (-1 == ret){perror("listen"); goto err1;}printf("server is waiting the client incomming....\n");//阻塞 接受客戶端連接newfd = accept(sockfd, (void*)&fromaddr, &len);if (-1 == newfd){perror("accept"); goto err1;}//客戶端的信息printf("from client ip: %s port: %d\n", inet_ntoa(fromaddr.sin_addr), ntohs(fromaddr.sin_port));//讀寫操作while(1){memset(buf, 0, SIZE); //ret = write(newfd, "hello world", 11); ret = send(newfd, "hello world", 11, 0);if (ret <= 0)break;printf("send %d bytes\n", ret);sleep(1);}close(sockfd);return 0; err1:close(sockfd); err0:return -1; }
Windows 的網絡調試助手作為 TCP 客戶端,給 ubuntu 中的服務器發送數據,運行結果如下:
關閉連接:close()
使用 close() 函數即可關閉套接字,關閉一個代表已連接套接字將導致另一端接收到一個 0 長度的數據包,詳情請看《 TCP 四次揮手》。
做服務器時
- 關閉監聽套接字( socket()和listen()之后的套接字 )將導致服務器無法接收新的連接,但不會影響已經建立的連接;
- 關閉 accept()返回的已連接套接字將導致它所代表的連接被關閉,但不會影響服務器的監聽( socket()和listen()之后的套接字 )。
做客戶端時
關閉連接就是關閉連接,不意味著其他。
如果客戶端和服務器已經連接成功的前提下,通常的情況下,先關閉客戶端,再關閉服務器,如果是先關閉服務器,立馬啟動服務器是,服務器綁定的端口不會立馬釋放(如下圖),要過 1 分鐘左右才會釋放,為什么會這樣的呢?請看《 TCP 四次揮手》。有沒有方法讓服務器每次啟動都能立即成功?請看《端口復用》。
總結
以上是生活随笔為你收集整理的【Linux网络编程】TCP的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Linux网络编程】Linux多播问题
- 下一篇: 【Linux网络编程】TCP网络编程中c