1. 程式人生 > >Linux 網路程式設計——TCP程式設計

Linux 網路程式設計——TCP程式設計

概述

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,代表 IPv4type:本示例寫 SOCK_STREAM,代表 TCP 資料流protocol:這裡寫 0,設為 0 表示使用預設協議
返回值:
成功:套接字失敗 < 0 

int connect( int sockfd, 

const struct sockaddr *addr, socklen_t len );

功能:

引數:

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

這裡通過 Windows 的網路除錯助手和虛擬機器中的 ubuntu 客戶端程式進行通訊,網路除錯助手下載請點此處

Windows 的網路除錯助手作為 TCP 伺服器,接收客戶端的請求,除錯助手配置如下:


虛擬機器中 ubuntu 的 TCP 客戶端程式程式碼:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
	unsigned short port = 8080;        		// 伺服器的埠號
	char *server_ip = "10.221.20.10";    	// 伺服器ip地址
	
	if( argc > 1 )		//函式傳參,可以更改伺服器的ip地址									
	{		
		server_ip = argv[1];
	}	
	if( argc > 2 )	   //函式傳參,可以更改伺服器的埠號									
	{
		port = atoi(argv[2]);
	}

	int sockfd;
	sockfd = socket(AF_INET, SOCK_STREAM, 0);// 建立通訊端點:套接字
	if(sockfd < 0)
	{
		perror("socket");
		exit(-1);
	}
	
	// 設定伺服器地址結構體
	struct sockaddr_in server_addr;
	bzero(&server_addr,sizeof(server_addr)); // 初始化伺服器地址
	server_addr.sin_family = AF_INET;	// IPv4
	server_addr.sin_port = htons(port);	// 埠
	inet_pton(AF_INET, server_ip, &server_addr.sin_addr);	// ip
	
	 // 主動連線伺服器
	int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));     
	if(err_log != 0)
	{
		perror("connect");
		close(sockfd);
		exit(-1);
	}
	
	char send_buf[512] = {0};
	printf("send data to %s:%d\n",server_ip,port);
	while(1)
	{
		printf("send:");
		fgets(send_buf,sizeof(send_buf),stdin); // 輸入內容
		send_buf[strlen(send_buf)-1]='\0';
		send(sockfd, send_buf, strlen(send_buf), 0);   // 向伺服器傳送資訊
	}

	close(sockfd);
	
	return 0;
}

執行結果如下:


對於客戶端,也是可以接收資料,前提為,客戶端先給伺服器傳送資料。

ssize_t recv(int sockfd, void *buf,  size_t nbytes, int flags);

功能:

接收網路資料,預設的情況下,如果沒有接收到資料,這個函式會阻塞,直到有資料到來。

引數:

sockfd套接字

buf:接收網路資料的緩衝區的地址

nbytes:接收緩衝區的大小(以位元組為單位)

flags:套接字標誌(常為 0 )

返回值:

成功:成功接收的位元組數

失敗 < 0

測試程式碼如下:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
	unsigned short port = 8080;        		// 伺服器的埠號
	char *server_ip = "10.221.20.10";    	// 伺服器ip地址
	
	if( argc > 1 )		//函式傳參,可以更改伺服器的ip地址									
	{		
		server_ip = argv[1];
	}	
	if( argc > 2 )	   //函式傳參,可以更改伺服器的埠號									
	{
		port = atoi(argv[2]);
	}

	int sockfd;
	sockfd = socket(AF_INET, SOCK_STREAM, 0);// 建立通訊端點:套接字
	if(sockfd < 0)
	{
		perror("socket");
		exit(-1);
	}
	
	// 設定伺服器地址結構體
	struct sockaddr_in server_addr;
	bzero(&server_addr,sizeof(server_addr)); // 初始化伺服器地址
	server_addr.sin_family = AF_INET;	// IPv4
	server_addr.sin_port = htons(port);	// 埠
	inet_pton(AF_INET, server_ip, &server_addr.sin_addr);	// ip
	
	 // 主動連線伺服器
	int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));     
	if(err_log != 0)
	{
		perror("connect");
		close(sockfd);
		exit(-1);
	}
	
	
	printf("send data to %s:%d\n",server_ip,port);
	
	char send_buf[512] = "Hi, I am Mike.";
	send(sockfd, send_buf, strlen(send_buf), 0);   // 向伺服器傳送資訊
	
	char recv_buf[512] = {0};
	recv(sockfd, recv_buf, sizeof(send_buf), 0); // 接收資料
	printf("recv_buf ========== %s\n", recv_buf);

	close(sockfd);
	
	return 0;
}

執行結果如下:


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 就固定了

引數:

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為通配地址,值為0

printf("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);

功能:

引數:

sockfd: socket監聽套接字

backlog:連線佇列的長度

返回值:

成功:返回0

失敗:其他

int accept(  int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen );

功能:

引數:

sockfd: socket監聽套接字

cliaddr: 用於存放客戶端套接字地址結構

addrlen:套接字地址結構體長度的地址

返回值:

成功:已連線套接字。注意:返回的是一個已連線套接字,這個套接字代表當前這個連線

失敗:< 0

ubuntu 中的伺服器程式碼如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>						
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>				
int main(int argc, char *argv[])
{
	unsigned short port = 8080;	// 本地埠	
	if(argc > 1)						
	{
		port = atoi(argv[1]);
	}
	
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);   // 建立通訊端點:套接字
	if(sockfd < 0)
	{
		perror("socket");
		exit(-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
	
	// 繫結
	int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
	if( err_log != 0)
	{
		perror("binding");
		close(sockfd);		
		exit(-1);
	}
	
	err_log = listen(sockfd, 10); // 監聽,監聽套接字改為被動
	if(err_log != 0)
	{
		perror("listen");
		close(sockfd);		
		exit(-1);
	}	
	
	printf("listen client @port=%d...\n",port);

	while(1)
	{	
	
		struct sockaddr_in client_addr;		   
		char cli_ip[INET_ADDRSTRLEN] = "";	   
		socklen_t cliaddr_len = sizeof(client_addr);    
		
		int connfd;
		// 等待連線
		connfd = accept(sockfd, (struct sockaddr*)&client_addr, &cliaddr_len);       
		if(connfd < 0)
		{
			perror("accept");
			continue;
		}

		inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
		printf("----------------------------------------------\n");
		printf("client ip=%s,port=%d\n", cli_ip,ntohs(client_addr.sin_port));
		
		char recv_buf[512] = "";
		while( recv(connfd, recv_buf, sizeof(recv_buf), 0) > 0 ) // 接收資料
		{
			printf("\nrecv data:\n");
			printf("%s\n",recv_buf);
		}
		
		close(connfd);     //關閉已連線套接字
		printf("client closed!\n");
	}
	
	close(sockfd);         //關閉監聽套接字
	
	return 0;
}

Windows 的網路除錯助手作為 TCP 客戶端,給 ubuntu 中的伺服器傳送資料,執行結果如下:


關閉連線:close()

使用 close() 函式即可關閉套接字,關閉一個代表已連線套接字將導致另一端接收到一個 0 長度的資料包,詳情請看《 TCP 四次揮手》


做伺服器時

  • 關閉監聽套接字( socket()和listen()之後的套接字 )將導致伺服器無法接收新的連線,但不會影響已經建立的連線;
  • 關閉 accept()返回的已連線套接字將導致它所代表的連線被關閉,但不會影響伺服器的監聽( socket()和listen()之後的套接字 )。

做客戶端時

關閉連線就是關閉連線,不意味著其他。

如果客戶端和伺服器已經連線成功的前提下,通常的情況下,先關閉客戶端,再關閉伺服器,如果是先關閉伺服器,立馬啟動伺服器是,伺服器繫結的埠不會立馬釋放(如下圖),要過 1 分鐘左右才會釋放,為什麼會這樣的呢?請看《 TCP 四次揮手》。有沒有方法讓伺服器每次啟動都能立即成功?請看《埠複用》