說明
調用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
幫助手冊裏面提供比較多,我們在這裏只說明一部分。
-
0:表示用不上flags,此時send是阻塞發送數據的,如果數據發送不成功就會阻塞,直到數據發送成功爲止,或者被信號中斷。send函數的flag參數設置爲0時,和write函數是功能一樣的。
-
MSG_NOSIGNAL:send發送數據時,如果對方將“連接”關閉掉了,調用send的進程會被髮送SIGPIPE信號,這個信號的默認處理方式是終止,所以收到這個信號的進程會被終止。
圖解分析:
如果不希望產生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函數監聽其他客戶連接。
所以上面程序只能夠監聽一個客戶。
我們在後面博客中詳細說明如何實現服務器爲多個客戶端服務。