服务器端:socket(), bind(), recvfrom()/sendto(), close();
客户端:socket(), sendto()/recvfrom(), close();
以下是各个函数的具体介绍:
首先提一句:linux下一切皆文件,socket也不例外,它是可读、可写、可控制、可关闭的文件描述符。
创建socket
#include
#include
int socket(int domain,int type,int protocol);//创建socket
socket()系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno。
命名(绑定)socket
#include
#include
int bind(int sockfd,const struct sockaddr* my_addr,socklen_t addlen);//命名(绑定)socket
bind的作用是将未命名的sockfd文件描述符指向my_addr所指的socket地址。其中socket地址长度由参数addlen指出。
bind成功时返回0,失败则返回-1并设置errno。
其中struct sockaddr是通用的socket地址,而TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于IPv4和IPv6,这里我们只介绍sockaddr_in:
struct sockaddr_in
{
sa_family_t sin_family; //地址族:AF_INET
u_int16_t sin_port; //端口号(要用网络字节序)
struct in_addr sin_addr; //IPv4地址结构体
};
struct in_addr //IPv4地址结构体
{
u_int32_t s_addr; //IPv4地址(要用网络字节序)
};
所有专用socket地址类型的变量在实际使用时都需要转化为通用的socket地址类型sockaddr(强转)。我们看到结构体中的端口号和IP地址都要求用网络字节序。
首先看端口号,两台主机之间要通过TCP/IP协议进行通信的时候需要调用相应的函数进行主机序 和网络序的转换。因为主机字节序一般为小端模式(Little-Endian),而网络字节序为大端模式(Big-Endian),也就是说两者的存储方式不同。所以我们介绍4个函数来完成主机字节序和网络字节序之间的转换:
#include
unsigned long int htonl(unsigned long int hostlong);//主机字节序转网络字节序(32bit的长整型)
unsigned short int htonl(unsigned short int hostshort);//主机字节序转网络字节序(16bit的短整型)
unsigned long int ntohl(unsigned long int netlong);//网络字节序转主机字节序(32bit的长整型)
unsigned short int ntohs(unsigned short int netshort);//网络字节序转主机字节序(16bit的短整型)
这些函数的含义很明确,第一个函数 htonl 表示“host to network long”,即长整型(32bit)的主机字节序转化为网络字节序。
接下来我们看IP地址,我们习惯用点分十进制这样的可读性好的字符串来表示IPv4地址,这里介绍3个IP地址转换函数:
#include
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp,struct in_addr* inp);
char* inet_ntoa(struct in_addr in);
inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。
inet_aton函数完成inet_addr函数同样的功能,但是将转化结果存储于参数inp指向的地址结构中。该函数成功时返回1,失败返回-1。
inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。
数据读写
对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。UDP与TCP不同,UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址。所以UDP的读写函数比TCP的读写函数参数要多。
#include
#include
int recvfrom(int sockfd,void* buf,size_t len,int flags,struct sockaddr* src_addr,socklen_t* addrlen);//读取sockfd上的数据
recvfrom用于读取sockfd上的数据。
recvfrom 成功时返回实际读取到的数据的长度(recv返回0时表示通信对方已经关闭连接),出错时返回-1并设置errno。
#include
#include
int sendto(int sockfd,const void* buf,size_t len,int flags,const struct sockaddr* dest_addr,socklen_t addrlen);//往sockfd上写入数据
sendto 是往sockfd上写入数据。
sendto 成功时返回实际写入的数据的长度(recv返回0时表示通信对方已经关闭连接),出错时返回-1并设置errno。
flags为数据收发提供额外的控制,flag参数的可选值如下表,这些选项的具体使用这里不作详述。
ps:recvfrom/sendto系统调用也可以用于面向连接的socket数据读写,只需要把最后两个参数都设置为NULL以忽略发送端、接收端的socket地址(因为我们已经建立了连接,所以已经知道其socket地址了)。
关闭连接
#include
int close(int fd);//关闭连接
close系统调用并非总是立即关闭一个连接,而是将fd的引用计数-1;只有当fd的引用计数为0时,c才真正关闭连接。
实现客户端输入数据,服务器端打印客户输入的数据,并且每次打印数据后给客户反馈。
服务器端代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd=socket(PF_INET,SOCK_DGRAM,0);//创建socket
assert(sockfd!=-1);
struct sockaddr_in cli,ser;
ser.sin_family=AF_INET;
ser.sin_port=htons(6000);
ser.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));//绑定socket
while(1)//保持服务器常驻
{
char buff[128]={0};
int len=sizeof(cli);
int n=recvfrom(sockfd,buff,127,0,(struct sockaddr*)&cli,&len);//接收客户端的数据
if(n<=0)
{
printf("recvfrom error\n");
continue;
}
printf("%s\n",buff);
sendto(sockfd,"OK",2,0,(struct sockaddr*)&cli,len);//给客户端回馈
}
close(sockfd);//关闭服务器
}
客户端代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int sockfd=socket(PF_INET,SOCK_DGRAM,0);//创建socket
assert(sockfd!=-1);
struct sockaddr_in ser,cli;
ser.sin_family=AF_INET;
ser.sin_port=htons(6000);
ser.sin_addr.s_addr=inet_addr("127.0.0.1");
while(1)//实现与服务器端多次交互
{
printf("please input:");
fflush(stdout);
char buff[128]={0};
fgets(buff,127,stdin);
buff[strlen(buff)-1]=0;
if(strncmp(buff,"end",3)==0)//客户输入end时关闭与服务器的交互
{
close(sockfd);//停止与服务器的连接
break;
}
sendto(sockfd,buff,127,0,(struct sockaddr*)&ser,sizeof(ser));//向服务器发送数据
memset(buff,0,128);
recvfrom(sockfd,buff,127,0,NULL,NULL);//相当于已建立连接,可用NULL
printf("%s\n",buff);
}
}
运行结果:
客户端:
服务器端: