TCP 通信服務器端實現第五步:調用read(recv)函數和 write(send)函數 ,收發數據,實現與客戶端的通信。【linux】(zzx)

說明

調用read(recv)函數和 write(send)函數 ,收發數據,實現與客戶端的通信。

read函數和write函數的用法我們在說明文件IO博客中時已經做了詳細的說明,我們這裏着重說明recv和send這兩個函數,recv和send其實和read和write差不多,它們的前三個參數都是一樣的,只不過recv和send多了第四個參數。

不管是使用read、write還是使用recv、send來實現TCP數據的收發,由於TCP建立連接時自動已經記錄下了對方的IP和端口,所以使用這些函數實現數據收發時,只需要指定通信描述符即可,不需要指定對方的ip和端口。

send函數

函數原型

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

功能

向對方發送數據。

其實也可以使用sendto函數,相比於send函數來說多了兩個參數,當sendto函數的後兩個參數寫NULL和0時,能完全等價於send函數。

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, NULL, 0);

類似TCP這種面向連接的通信,我們一般使用send而不是使用sendto,因爲sendto函數多了兩個參數,用起來有點麻煩。

類似UDP這種不需要連接的通信,必須使用sendto,不能使用send,我們後面在說明UDP通信的時候進行詳細說明。

返回值

成功

返回發送的字節數。

失敗

返回-1,ernno被設置。

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

參數

sockefd

用於通信的通信描述符

不要因爲參數名字寫的是sockfd,就認爲一定是socket函數返回“套接字文件描述符”。

socket函數返回的描述符是用來監聽用的。

對於服務器這邊來說,accept函數返回的纔是通信描述符,所以服務器調用send發送數據時,第一個參數寫accept所返回的通信描述符。

buf

應用緩存,用於存放要發送的數據,可以是任何你想發送的數據,比如結構體、int、float、字符、字符串等等。

正規操作的話,應該使用結構體來封裝數據。

len

buf緩存的大小。

flags

一般設置0,表示這個參數用不到,此時send是阻塞發送的。

阻塞發送的意思就是,如果數據發送不成功會一直阻塞,直到被某信號中斷或者發送成功爲止,不過一般來說,發送數據是不會阻塞的。

當flags設置0時,send與write的功能完全一樣,有關flags的其它設置,我們後面進行說明。

代碼演示說明

數據的發送和接收,這篇博客我們說明是send函數,所以在這裏演示數據的發送。下一篇博客在說明recv函數時我們說明數據的接收。

收發的數據需要在網路中傳輸,所以同樣要進行端序的轉換。

發送數據:將主機端序轉爲網絡端序。
接收數據:將網絡端序轉爲主機端序。

不過只有short、int、float等存儲單元字節>1字節的數據,纔有轉換的需求。
如果是char這種存儲單元爲一個字節的數據,不需要對端序進行轉換。

疑問:爲什麼存儲單元爲一個字節的數據,不需要進行端序的轉換呢?
有關這個問題,我們在C語言博客中已經詳細說明所以這裏不再贅述。

我們這裏雖然說明的是C程序的例子,但是實際上c++/java等調用相應的網絡API實現通信時,應用程序同樣需要進行端序的轉換,與我們這說明的c程序的情況是一樣的。

前面已經說過,本機測試時其實是可以不進行端序轉換的,但是對於正規網絡通信來說,收發計算機的大小端序不一定相同,爲了避免因端序不同所帶來的通信錯誤,我們應該嚴格操作,收發數據時必須對端序進行轉換。

代碼演示:服務器發送數據

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SPROT 5006
#define SIP "192.168.31.162"

//封裝發送的應用層數據:學生信息
typedef struct _data
{
	unsigned int stu_num;		//學號	
	char stu_name[50];	//姓名
}Data;
void  print_err(char * str,int line,int err_no)//出錯處理函數
{
	printf("%d,%s: %s\n",line,str,strerror(err_no));
	exit(-1);
}
int main(void)
{
	int ret = -1;
	int sockfd = -1; //存放套接字文件描述符
	/*創建使用TCP協議通信的套接字文件*/
	sockfd =  socket(PF_INET,SOCK_STREAM,0); //指定TCP協議
	if(-1 == sockfd) //進行出錯處理
		print_err("socket fail",__LINE__,errno);

	/*調用bind函數綁定套接字文件/ip/端口*/
	struct sockaddr_in saddr;
	saddr.sin_family = AF_INET;//指定ip格式
	saddr.sin_port= htons(SPROT);//指定端口
	saddr.sin_addr.s_addr = inet_addr(SIP);//設置ip
	ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//綁定
	if(-1 ==ret)
		print_err("bind fail",__LINE__,errno);

	/*將主動的套接字文件描述符轉換爲被動的文件描述符,用於被動監聽客戶連接。*/
	ret = listen(sockfd,3);
	if(-1 ==ret)
		print_err("listen fail",__LINE__,errno);
	
	/*調用accep函數,被動監聽客戶的連接*/
	int cfd = 0; /*存放與客戶通信的通信文件描述符*/
	struct sockaddr_in clnaddr = {0};/*用來保存連接客戶端的IP和端口*/
	int clnaddr_size  = sizeof(clnaddr);/*存放結構體變量的大小*/
	cfd = accept(sockfd,(struct sockaddr *)&clnaddr,&clnaddr_size);
	if(-1 ==ret)
		print_err("accept fail",__LINE__,errno);
	/*打印客戶端的端口和ip,一定要記得進行端序轉換*/
	printf("client_port = %d\n,client_ip = %s\n",ntohs(clnaddr.sin_port),inet_ntoa(clnaddr.sin_addr));
	
	/*循環發送數據*/
	Data stu_data = {0};
	int tmp_num;
	while(1)
	{
		/*獲取學生的學號,但是需要將學號從主機端序轉換爲網絡端序*/
		printf("input student number:\n");
		scanf("%d",&tmp_num);
		stu_data.stu_num = htonl(tmp_num);

		/*char類型不需要端序轉換*/	
		printf("input student name:\n");
		scanf("%s",stu_data.stu_name);

		ret = send(cfd,(void *)&stu_data,sizeof(stu_data),0);
		if(-1 ==ret)
			print_err("send fail",__LINE__,errno);
		
	}
	return 0;
}

編譯沒問題,但是現在還不能演示,在後面客戶端和服務器端都寫完之後配合運行我們才能進行演示。

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

flags的常見設置

我們通過man send 命令查看flags的設置值。
命令:man send

flag設置

幫助手冊裏面提供比較多,我們在這裏只說明一部分。

  • 0:表示用不上flags,此時send是阻塞發送數據的,如果數據發送不成功就會阻塞,直到數據發送成功爲止,或者被信號中斷。send函數的flag參數設置爲0時,和write函數是功能一樣的。

  • MSG_NOSIGNAL:send發送數據時,如果對方將“連接”關閉掉了,調用send的進程會被髮送SIGPIPE信號,這個信號的默認處理方式是終止,所以收到這個信號的進程會被終止。
    圖解分析:
    MSG_NOSIGNAL圖解說明

如果不希望產生SIGPIPE信號,給flags指定MSG_NOSIGNAL,表示當連接被關閉時不會產生該信號。

從這裏可看出,並不是只有寫管道失敗時纔會產生SGIPIPE信號,網絡通信時也會產生這個的信號。

  • MSG_DONTWAIT:send函數非阻塞發送數據。如果send函數發送數據成功,就發送成功,如果發送不成功會出錯返回而不會阻塞。

  • MSG_OOB:表示發送的是帶外數據
    有關帶外數據我們在後面進行說明。

以上除了0以外,其它選項可以 | 操作,比如MSG_DONTWAIT | MSG_OOB。

recv函數

函數原型

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

功能

接收對方發送的數據

我們也可以使用rcvfrom函數,當recvfrom函數的最後兩個參數寫NULL和0時,與recv的功能完全一樣。

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, NULL, 0);

同樣的,在udp通信博客中我們詳細說明recvfrom函數。

UDP通信在發送數據的時候必須使用sendto函數,接收數據的時候必須使用recvfrom函數。

返回值

成功返回接收的字節數
失敗返回-1,ernno被設置

參數

sockfd

通信文件描述符

buf

應用緩存,用於存放接收的數據

len

buf的大小

flags

  • 0:默認設置,此時recv是阻塞接收的,0是常設置的值。Recv函數當把flag設置爲0時和raed函數是等價的。

  • MSG_DONTWAIT:非阻塞接收。

  • MSG_OOB:接收的是帶外數據。

其他flag設定值,讀者可以在recv函數的幫助手冊中進行詳細查看。

代碼演示:服務器接收數據

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#define SPROT 5006
#define SIP "192.168.31.162"

//封裝發送的應用層數據:學生信息
typedef struct _data
{
	unsigned int stu_num;		//學號	
	char stu_name[50];	//姓名
}Data;

void  print_err(char * str,int line,int err_no)//出錯處理函數
{
	printf("%d,%s: %s\n",line,str,strerror(err_no));
	exit(-1);
}


int cfd = -1; /*存放與客戶通信的通信文件描述符*/

/*次線程用於接收客戶端的數據*/
void * pth_fun(void * pth_arg)
{
	int ret = 0;
	Data stu_data = {0}; //用於存放接收對方發送過來的學生數據

	while(1)
	{
		bzero(&stu_data,sizeof(stu_data));
		ret = recv(cfd,&stu_data,sizeof(stu_data),0);//接收對方發送過來的數據
		if(-1 == ret) //進行出錯處理
			print_err("recv fail",__LINE__,errno);
		printf("student number = %d\n",ntohl(stu_data.stu_num));
		printf("student name = %s\n",stu_data.stu_name);
	}
}
int main(void)
{
	int ret = -1;
	int sockfd = -1; //存放套接字文件描述符
	/*創建使用TCP協議通信的套接字文件*/
	sockfd =  socket(PF_INET,SOCK_STREAM,0); //指定TCP協議
	if(-1 == sockfd) //進行出錯處理
		print_err("socket fail",__LINE__,errno);

	/*調用bind函數綁定套接字文件/ip/端口*/
	struct sockaddr_in saddr;
	saddr.sin_family = AF_INET;//指定ip格式
	saddr.sin_port= htons(SPROT);//指定端口
	saddr.sin_addr.s_addr = inet_addr(SIP);//設置ip
	ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//綁定
	if(-1 ==ret)
		print_err("bind fail",__LINE__,errno);

	/*將主動的套接字文件描述符轉換爲被動的文件描述符,用於被動監聽客戶連接。*/
	ret = listen(sockfd,3);
	if(-1 ==ret)
		print_err("listen fail",__LINE__,errno);
	
	/*調用accep函數,被動監聽客戶的連接*/
	struct sockaddr_in clnaddr = {0};/*用來保存連接客戶端的IP和端口*/
	int clnaddr_size  = sizeof(clnaddr);/*存放結構體變量的大小*/
	cfd = accept(sockfd,(struct sockaddr *)&clnaddr,&clnaddr_size);
	if(-1 ==ret)
		print_err("accept fail",__LINE__,errno);
	/*打印客戶端的端口和ip,一定要記得進行端序轉換*/
	printf("client_port = %d\n,client_ip = %s\n",ntohs(clnaddr.sin_port),inet_ntoa(clnaddr.sin_addr));
	
	/*創建次線程用於接收對方發送的數據*/
	pthread_t tid;
	pthread_create(&tid,NULL,pth_fun,NULL);
	/*循環發送數據*/
	Data stu_data = {0};
	int tmp_num;
	while(1)
	{
		/*獲取學生的學號,但是需要將學號從主機端序轉換爲網絡端序*/
		printf("input student number:\n");
		scanf("%d",&tmp_num);
		stu_data.stu_num = htonl(tmp_num);

		/*char類型不需要端序轉換*/	
		printf("input student name:\n");
		scanf("%s",stu_data.stu_name);

		ret = send(cfd,(void *)&stu_data,sizeof(stu_data),0);
		if(-1 ==ret)
			print_err("send fail",__LINE__,errno);
		
	}
	return 0;

}

上面的服務器只能爲一個客戶服務,因爲一旦監聽到某個客戶連接成功之後,後面就會直接進入死循環,服務器端程序將不再執行accept函數監聽其他客戶連接。

所以上面程序只能夠監聽一個客戶。
我們在後面博客中詳細說明如何實現服務器爲多個客戶端服務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章