文章目录
Udp编程接口
socket():创建套接字
- 1.使用socket()函数创建一个文件描述符
int socket(AF_INET,SOCK_DGRAM,0);
//参数一:表示改文件描述符使用的是IPV4
//参数二:表示该文件描述符使用的是UDP协议
//参数三:一般不使用
-
参数domain 用来说明网路程序所在的主机采用的是那种通信协议簇,这些协议族再头文件<sys/socket.h>中定义
-
AF_INET //表示IPv4网络协议
-
AF_INET6 //IPv6网络协议
-
参数type 用来指明创建的套接字类型,
-
SOCK_STREAM:流式套接字,面向连接可靠的通信类型
-
SOCK_DGRAM:数据报套接字,非面向连接和不可靠的通信类型
-
SOCK_RAW:原始套接字,用来直接访问IP协议
-
参数protocol 指定套接字使用的协议,一般采用默认值0,表示让系统根据地址格式和套接字类型,自动选择一个合适的协议
-
返回值:
-
调用成功就创建了一个新的套接字并返回它的描述符,在之后对该套接字的操作中都要借助这个文件描述符,
-
否则返回-1(<0) 表示套接字出错.应用程序可调用WSAGetLastError() 获取相应的错误代码
bind ():将套接字绑定到指定的网络地址
使用这个函数前需要将服务器的ip和端口号赋值到结构体sockaddr_in中
使用bind()函数前的准备工作
- sockaddr 结构:针对各种通信域的套接字,存储它们的地址信息
struct sockaddr{
u_short sa_family; /*16位协议家族*/
char sa_data[14] /*14字节协议地址*/
};
- sockaddr_in结构:专门针对Internet通信域,存储套接字相关的网络地址信息(IP地址、传输层端口号等信息)
struct sockadd_in{
short int sin_family; //地址家族
unsigned short int sin_port; //端口号
struct in_addr sin_addr; //IP地址
unsigned char sin_zero[8]; //全为0
};
- in_addr 结构:专门用来存储IP地址
struct in_addr{
unsigned long s_addr;
};
#include<netinet/in.h>
int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
-
参数sockfd:是未经绑定的套接字文件描述符,是由socket()函数返回的,要将它绑定到指定的网络地址上
-
参数addr : 是一个指向sockaddr结构变量的指针,所指结构体中保存着特定的网络地址,就是要把套接字sockfd绑定到这个地址上.
-
参数 addrlen :是结构sockaddr结构的长度,等于sizeof(struct sockadd
-
返回值
返回0:表示已经正确的实现了绑定
如果返回SOCKET_ERROR表示有错。应用程序可调WSAGetLastError()获取相应的错误代码
- 最后在函数调用的时候,将这个结构强制转换成sockaddr类型
本机字节序和网络字节序
-
本机字节序在不同的计算机中,存放多字节值的顺序是不一样的,有的是从低到高,有的是从高到低.计算机中的多字节数据的存储顺序称为本机字节顺序.
-
网络字节序:在网络协议中,对多字节数据的存储,有它自己的规定。多字节数据在网络协议报头中的存储顺序,称为网络字节序.在套接字中必须使用网络字节序
所以把IP地址和端口号装入套接字时,我们需要将本机字节序抓换成网络字节序,在本机输出时,我们需要将它们从网络字节序转换成本机字节序
-
套接字编程接口专门为解决这个问题设置了4个函数
-
1.htons():短整数本机顺序转换为网络顺序,用于端口号
-
2.htonl():长整数本机顺序转换成网络顺序,用于IP地址
-
3.ntohs():短整数网络顺序转换为本机顺序,用于端口号
-
4.ntohl():长整数网络顺序转换为本机顺序,用于IP地址
这4个函数将被转换的数值作为函数的参数,函数返回值作为转换后的结果
- 点分十进制的IP地址的转换
在Internet中,IP地址常常是用点分十进制的表示方法,但在套接字中,IP地址是无符号的长整型数,套接字编程接口设置了两个函数,专门用来两种形式IP地址的转换
- 1.inet_addr函数
unsigned long inet_addr(const char* cp);
/*
入口参cp:点分十进制形式的IP地址
返回值:网络字节顺序的IP地址,是无符号的长整数
*/
- 2.inet_ntoa函数
char* inet_ntoa(struct in_addr in)
/*
入口参数in: 包含长整数IP地址的in_addr结构变量
返回值:指向点分十进制IP地址的字符串的指针
*/
recvfrom()接收一个数据报并保存源地址,从数据报套接字接收数据
调用格式
int recvfrom(SOCKET s,char *buf,int len,int falgs,
struct sockaddr* from, int* fromlen);
-
参数s:接收端的数据报套接字描述符,包含接收方的网络地址,从这个套接字接收数据报
-
参数buf:字符串指针,指向用户进程的接收缓冲区,用来接收从套接字收到的数据报
-
参数len:用户接收缓冲区的长度,制定了所能接收的最大字节数
-
参数flags:接收的方式,一般置为0.
-
参数from: 指向sockaddr 结构的指针,实际上是一个出口参数,当本次调用成功执行后,在这个结构中返回了发送方的网络地址,包括对方的IP地址和端口号
-
参数fromlen:整数型指针,也是一个出口型参数,本调用结束时,返回存在from中的网络地址长度
-
返回值: 如果正确接收,则返回实际收到的字节数,如果出错,返回SOCKET_ERROR,应用程序可通过WSAGetLastError()获取相应的错误代码
函数功能
- 本函数从s套接口 的接收缓冲区队列中,取出第一个数据报,把它放到用户进程 的缓冲区buf中,但最多不超过用户缓冲区的大小。如果数据报大于用户缓冲区长度,那么用户缓冲区中只有数据报的前面部分,后面的数据都会丢失,并且recvfrom()函数WSAEMSGSIZE错误
sendto():按照指定目的地向数据报套接字发送数据
调用格式
int sendto(SOCKET s,char* buf,int len,int flags,struct sockaddr* to,
int tolen)
-
参数s:发送方的数据报套接字 描述符,包含发送方的网络地址,数据报通过这个套接字向对方发送
-
参数buf:指向用户进程发送缓冲区的字符串指针,该缓冲区包含将要发送的数据,
-
参数len:用户发送缓冲区中要发送的数据的长度,是可以发送的最大字节数
-
参数flags: 指定函数的执行方式,一般置为0.
-
参数to:指向sockaddr 结构的指针,指定接收数据报的目的套接字的完整的网络地址
-
参数tolen: to的地址长度,等于sizeof(struct sockaddr)
-
返回值:如果发送成功,则返回实际发送的字节数,注意这个数字可能小于len中所规定的长度,如果出错返回错误码
函数功能
本函数专用于数据报套接字,用来向发送端的本地套接字发送一个数据报.套接字会将数据下交给传输层的UDP,由它向对方发送.
- 容易看出,这个调用需要决定通信的两个端点,需要一个全相关的五元组信息,既协议(UDP)、源IP地址、源IP地址、源端口号、目的IP地址和目的端口.通信一端由发送方套接字s指定,通信的另一端由to结构决定
实例代码
udp_server.cc
//先实现UDP版本的服务器
#include<stdio.h>
#include<sys/socket.h> //socket所用的头文件
#include<netinet/in.h> //socketaddr_in 结构体的头文件
#include<arpa/inet.h> //inet.addr的头文件
#include<cstring>
#include<unistd.h>
int main(){
//1.先创建一个socket
// AF_INET是一个宏 ,表示使用 ipv4 协议
//SOCK_DGRAM 表示使用 UDP 协议
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
perror("socket");
return 1;
}
//2.把当前的socket 绑定上一个ip + 端口号
sockaddr_in addr; //是一个结构体
addr.sin_family=AF_INET;
addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0将本地所有的ip都包含进来
//把点分十进制转换成32位整数
//设置ip地址
//ip 地址也是一个整数,也需要转成网络字节序,只不过
//int_addr 函数自动帮我们转了
addr.sin_port=htons(8080);
//端口号必须得先转成网络字节序
//9090 TCP端口,这个端口不是系统默认的端口,
//属于自定义的端口
int ret=bind(sock,(sockaddr*)&addr,sizeof(addr));
if(ret<0){
perror("bind");
return 1;
}
printf("server start ok!\n");
//3.处理服务器收到的请求
while(true){
//1.读取客户端的请求
//面向数据报的函数
sockaddr_in peer;//获取客户端的ip
socklen_t len=sizeof(peer);//表示一个整数
char buf[1024]={0};
ssize_t n= recvfrom(sock,buf,sizeof(buf)-1,0,(sockaddr*)&peer,&len);
printf("客户端发来消息:%s\n",buf);
printf("请输入回复内容:");
if(n<0){
perror("recvfrom");
continue;//不要因为一次请求的失利就结束整个程序
}
buf[n]='\0';
//double check
//2.根据请求计算响应时间
//[略]此处写的是一个回显服务器(echo server)
//3.把响应写回客户端
char buf2[1024]="0";
scanf("%s",buf2);
n=sendto(sock,buf2,strlen(buf2),0,(sockaddr*)&peer,len);
if(n<0){
perror("sendto");
continue;
}
printf("[%s:%d] buf: %s\n",inet_ntoa(peer.sin_addr),
ntohs(peer.sin_port), buf2);
}
close(sock);
return 0;
}
ucp_client.cc
//实现UDP版本的客户端
#include<stdio.h>
#include<sys/socket.h> //socket所用的头文件
#include<netinet/in.h> //socketaddr_in 结构体的头文件
#include<arpa/inet.h> //inet.addr的头文件
#include<cstring>
#include<unistd.h>
//./client [ip]
int main(int argc,char *argv[]){
//1.先创建一个socket
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
perror("socket");
return 1;
}
//2.客户端一般不需要bind
//bind 意味着和某个具体的端口号关联在一起
//如果没有bind,操作系统会随机分配一个端口号
//如果是服务器程序不去bind ,就会导致每次启动服务器的端口发生变化
//客户端就没法连接了
//如果客户端bind的话,可能会出现问题
//通常情况下,一个端口号只能被一个进程bind
//如果客户端bind一个端口号其他客户端也可能bind同一个端口号
//就会出错
//客户端最好还是让操作系统随机分配更科学
//2.准备好服务器的sockaddr_in 结构
sockaddr_in server_addr;
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=inet_addr(argv[1]);
server_addr.sin_port=htons(8080);
//3.客户端直接发送数据即可
while(1){
char buf[1024]={0};
printf("请输入一段内容:");
fflush(stdout);
scanf("%s",buf);
sendto(sock,buf,strlen(buf),0,
(sockaddr*)&server_addr,sizeof(server_addr));
//从服务器接收一下返回结果
char buf_output[1024]={0};
//后两个参数填NULL 表示不关注对端的地址
recvfrom(sock,buf_output,sizeof(buf_output)-1,0,
NULL,NULL);
printf("客户端回应:%s\n",buf_output);
}
return 0;
}