2-3:套接字(Socket)编程之UDP通信,sockaddr,sockaddr_in,recvfrom,sendto
Posted 快乐江湖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2-3:套接字(Socket)编程之UDP通信,sockaddr,sockaddr_in,recvfrom,sendto相关的知识,希望对你有一定的参考价值。
文章目录
一:回顾
前文讲过,套接字分为流式套接字(SOCKET_STREAM)和数据报套接字(SOCKET_DGRAM),他们所采用的协议分别为TCP和UDP,相应的对应的Socket编程就是TCP套接字编程和UDP套接字编程
相比于UDP而言,TCP保证了数据的可靠传输,所以它比UDP就复杂一点,从下面的流程图中也可以看出来
- 流式套接字
- 数据包套接字
二:彻底了解套接字和struct socket结构
(1)一切皆文件-文件描述符-套接字描述符
下图是Linux内核中关于socket的数据结构还有后续我们再说编程时的一些API接口。
你可能注意到了一个非常熟悉的地方,struct file* file
,这不就是文件吗?是的没错,如果再深入理解一点,其实套接字就是使用文件描述符和其它程序进行通讯的一种方式。我们知道,Linux系统在执行任何I/O的时候,都在和文件描述符打交道,而在Linux下,我们一直反复强调“一切皆文件的思想”,之前说过的屏幕都可以作为文件,那么现在接触的网卡也当然可以做文件
所以以后再进行网络通讯时,你会利用socket系统调用,它将返回套接字文件描述符,然后你会利用它再通过相应(如下)接口进行通信操作。
既然它是文件描述符,那就意味着你仍然可以使用read()和write()来进行通信,但是“术业有专攻”,网络的事情还是尽量使用它们对应的接口来操作。
(2)struct socket结构
A:struct socket结构体作用
用户使用socket系统调用编写程序时,通过套接字描述符完成相关操作
int socket(int domain, int type, int protocol);
它对应的就是我们在上面说到的struct socket结构体
那么内核中为什么要有struct socket这样的结构体呢,以及它有什么作用呢?可以看下面这张图
所以内核中的进程可以通过该结构体来访问Linux内核中的传输层,网络层和数据链路层,也就是说struct socket是内核中的进程与内核中的网络系统的桥梁
B:struct socket结构体详解
这是一个基本的BSD socket,我们调用socket系统调用创建的各种不同类型的socket,开始创建的都是它,到后面**,各种不同类型的socket在它的基础上进行 各种扩展。struct socket是在虚拟文件系统上被创建出来的,可以把它看成一个文件,**是可以被安全地扩展的。下面是其完整定义:
struct socket {
socket_state state;
unsigned long flags;
const struct proto_ops *ops;
struct fasync_struct *fasync_list;
struct file *file;
struct sock *sk;
wait_queue_head_t wait;
short type;
};
1: state用于表示socket所处的状态,是一个枚举变量,其类型定义如下:
该成员只对TCP socket有用,因为只有tcp是面向连接的协议,udp跟raw不需要维护socket状态。
typedef enum {
SS_FREE = 0, //该socket还未分配
SS_UNCONNECTED, //未连向任何socket
SS_CONNECTING, //正在连接过程中
SS_CONNECTED, //已连向一个socket
SS_DISCONNECTING //正在断开连接的过程中
}socket_state;
2:ops是协议相关的一组操作集,结构体struct proto_ops的定义如下:
struct proto_ops {
int family;
struct module *owner;
int (*release)(struct socket *sock);
int (*bind)(struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);
int (*connect)(struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags);
int (*socketpair)(struct socket *sock1, struct socket *sock2);
int (*accept)(struct socket *sock,struct socket *newsock, int flags);
int (*getname)(struct socket *sock, struct sockaddr *addr,int *sockaddr_len, int peer);
unsigned int (*poll)(struct file *file, struct socket *sock,
struct poll_table_struct *wait);
int (*ioctl)(struct socket *sock, unsigned int cmd, unsigned long arg);
int (*listen)(struct socket *sock, int len);
int (*shutdown)(struct socket *sock, int flags);
int (*setsockopt)(struct socket *sock, int level,
int optname, char __user *optval, int optlen);
int (*getsockopt)(struct socket *sock, int level,
int optname, char __user *optval, int __user *optlen);
int (*sendmsg)(struct kiocb *iocb, struct socket *sock,
struct msghdr *m, size_t total_len);
int (*recvmsg)(struct kiocb *iocb, struct socket *sock,
struct msghdr *m, size_t total_len, int flags);
int (*mmap)(struct file *file, struct socket *sock,struct vm_area_struct * vma);
ssize_t (*sendpage)(struct socket *sock, struct page *page,
int offset, size_t size, int flags);
};
其中协议栈总共定义了了三个strcut proto_ops
类型的变量,分别myinet_stream_ops,
myinet_dgram_ops
, myinet_sockraw_ops
,分别对应流式套接字,数据报套接字和原生套接字
3:type是socket的类型,对应的取值如下:
enum sock_type {
SOCK_DGRAM = 1, //数据报套接字
SOCK_STREAM = 2, //流式套接字
SOCK_RAW = 3, //原生套接字
SOCK_RDM = 4,
SOCK_SEQPACKET = 5,
SOCK_DCCP = 6,
SOCK_PACKET = 10,
};
4:sk是网络层对于socket的表示,用户需要进行传参,让网络层采用TCP还是UDP
三:socket接口(UDP)和sockaddr结构
(1)socket常用API接口1(UDP)
所以我们要完成通讯,就要利用sockt结构体提供给我们的一些接口来实现。由于UDP只负责传送,所以相较于TCP而言它的接口少一点,所以这里只列出UDP中使用到的,但需要注意的是下面的接口对TCP也是通用的,只不过TCP相较于UDP的接口要多一点,还要扩展一点。
本节的模型就是一个服务端接受客户端发送的消息,然后回应客户端
1:创建 一个socket 文件描述符
#include <sys/tyeps.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
他们三个参数的含义及选用如下
对于它的返回值其实前面我们已经说过了,本质是一个文件描述符
2:绑定地址信息
前文说过,IP地址+端口号唯一表示了全网的一个进程。服务端既然想要提供服务,那么必须要让客户端知道怎么找到自己。所以对于服务端我们需要填入ip地址和端口号,以便客户端可以找到自己。 当然客户端一般是不要绑定的,当数据返回给客户端时,具体要用哪一个端口,是操作系统决定的,如果我们人为去绑定,可能导致端口号的冲突
#include <sys/types.h>
#include <sys/socket.h>
int bind(int socket,const struct sockaddr* address,socklen_t addrss_len);
第一个参数很好理解,第二个参数和第三个参数就是我们下面要说到的sockaddr结构
(2)sockaddr结构
所以在绑定时,我们需要将ip和端口号一起封装在某个结构体中,然后传参到bind接口里面。它共有三种类型的结构体
你可能发现了,bind接口的形参给的是struct sockaddr*的指针,但是为什么这里有三种类型的结构呢。其实这样的设计主要是为了用更少的操作完成更多的事情。
在传参时,这三种结构体的前16位是不相同的,它就是通过这个来区分的,所以只要保证在对齐的情况下,就能用struct sockaddr*来接受不同的结构,这有点像C++中的切片操作
A:struct sockaddr结构
struct sockaddr为许多类型的套接字存储套接字地址信息,其结构如下
struct sockaddr {
unsigned short sa_family; /* 地址家族, AF_xxx */
char sa_data[14]; /*14字节协议地址*/
};
这个结构体是一个通用的地址信息结构,它并不是某一个具体的地址信息结构。所以我们不选择它,而选择struct sockaddr_int这种结构
B:struct sockaddr_in
该结构体如下
struct sockaddr_in {
short int sin_family; /* 通信类型 */
unsigned short int sin_port; /* 端口 */
struct in_addr sin_addr; /* Internet 地址 */
unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/
};
用这样结构可以很轻松的处理套接字地址的基本元素。
现在让我们回到bind接口,看一下它是如何传参的:当我们使用ipv4版本的洗衣,绑定地址信息的时候,需要填充struct sockadd_in结构体来保存服务器的ip和端口号
四:UDP通信示例
(1)UDP通信
UDP是面向非连接的协议,它不与对方建立连接,而是直接把数据报发给对方。UDP无需建立类如三次握手的连接,使得通信效率很高。因此UDP适用于一次传输数据量很少、对可靠性要求不高的或对实时性要求高的应用场景。
UDP服务端流程如下
- 使用函数socket(),生成套接字文件描述符
- 通过struct sockaddr_in 结构设置服务器地址和监听端口
- 使用bind() 函数绑定监听端口,将套接字文件描述符和地址类型变量(struct sockaddr_in )进行绑定;
- 接收客户端的数据,使用recvfrom() 函数接收客户端的网络数据;
- 向客户端发送数据,使用sendto() 函数向服务器主机发送数据;
- 关闭套接字,使用close() 函数释放资源;
UDP客户端流程如下
- 使用函数socket(),生成套接字文件描述符
- 通过struct sockaddr_in 结构设置服务器地址和监听端口
- 向服务器发送数据,sendto() ;
- 接收服务器的数据,recvfrom() ;
- 关闭套接字,close() ;
需要注意以下几点
-
服务器和客户端地址理应是不一样的,这里为了测试使用本地环回地址
-
ip地址实际是点分十进制,每个部分算作1个字节,但是我们输入时往往是字符串,所以下面的接口可以将字符串转换为正确的IP地址,并且是网络字节序
-
如果需要将四字节序列转为点分十进制,则用
char *inet_ntoa (struct in_addr);
-
本地环回:本地环回地址为127.0.0.1,这是一个测试IP。表示数据会完整的走一遍协议,但是是自己发自己收
-
对于服务端一般不指定某个ip,因为有可能会有很多个ip,如果指定了ip,服务端只能接受特定ip的数据。所以我们一般把ip设置为一个宏,也即
INADDR_ANY
表示绑定任意IP
(1)sendto和recvfrom接口
1:UDP发送函数
#include <sys/types.h>
#include <sys/socket.h>
int sendto(
int sockfd,//套接字描述符
const void* buf,//要发什么东西
size_t len,//期望发多长
,int flags,//阻塞还是非阻塞,一般设置为0
const struct sockaddr* dest_addr,//指向服务器的struct_in结构体(注意强转)
socklen_t addrlen//指的是上面结构体的长度
)
2:UDP接受函数
#include <sys/tyeps.h>
#include <sys/socket.h>
ssize_t recvfrom(
int sockfd,//套接字描述符
void* buf,//读取到放在哪?
size_t len,//期望读取多长
int flags.//没有数据读的时候挂起,默认设置为0表示阻塞等待
struct sockaddr* src_addr,
socklen_t* addrlen//保存那个客户端发给你的(这两个如果你不关心是谁发给你的,设置为null即可。)
(2)代码
udpServer.h
#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
class udpServer
{
private:
int _port;//端口号
int _sock;//套接字描述符
public:
udpServer(int port=8080):_port(port)
{
}
void initServer()//初始化服务器
{
_sock=socket(AF_INET,SOCK_DGRAM,0);//
cout<<_sock<<endl;
struct sockaddr_in local;//创建sockaddr_in结构
//填充
local.sin_family=AF_INET;//IPV4协议
local.sin_port=htons(_port);//主机字节序转为网路字节序
local.sin_addr.s_addr=INADDR_ANY;//绑定任意IP
//绑定
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
cerr << "绑定失败" <<endl;
exit(1);
}
}
void startServer()
{
char msg[64];
for(;;)//服务器永不停机
{
msg[0]='\\0';//清空缓冲区
struct sockaddr_in end_point;//客户端的信息
socklen_t len=sizeof(end_point);//输出和输入型参数,recvfrom和sendto都要用
ssize_t ret=recvfrom(_sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);//接受,并把客户端的信息保存在结构体当中
if(ret > 0)
{
char bu[16];
sprintf(bu,"%d",ntohs(end_point.sin_port));//把网络字节序转为主机字节序,端口号
string cli=inet_ntoa(end_point.sin_addr);//把客户端的ip转为点分十进制
cli+=":";
cli+=bu;
msg[ret]='\\0';
cout<<"服务端接受到消息:来自->"<<cli<<" "<< msg << endl;
string respond="服务端回消息";
sendto(_sock,respond.c_str(),respond.size(),0,(struct sockaddr*)&end_point,len);//服务器应答
}
}
}
~udpServer()
{
close(_sock);
}
};
udpServer.cpp
#include "udpServer.h"
int main(int argc,char* argv[])
{
if(argc!=2)//判断是否传入端口号
{
cout<<"端口号未传入"<<endl;
exit(1);
}
udpServer* ss=new udpServer(atoi(argv[1]));
ss->initServer();
ss->startServer();
}
udpClient.h
#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
class udpClient
{
private:
string _ip;//
int _port;//客户端要保存服务器的IP和端口
int _sock;//套接字描述符
public:
udpClient(string ip="127.0.0.1",int port=8080):_ip(ip),_port(port)
{
//连接服务器,本地环回测试
}
void initClient()//初始化服务器
{
_sock=socket(AF_INET,SOCK_DGRAM,0);//
//客户端不需要绑定
}
void startClient()
{
string msg;//接受用户输入
//发送给服务器
struct sockaddr_in peer;
peer.sin_family=AF_INET;
peer.sin_port=htons(_port);
peer.sin_addr.s_addr=inet_addr(_ip.c_str());
for(;;)
{
cout<<"【请输入:】";
cin>>msg;
if(msg=="quit")
break;//如果用户输入退出,下线
sendto(_sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));//客户端给服务端发送消息
//服务器接受消息会返回信息
char echo[128];
ssize_t ret=recvfrom(_sock,echo,sizeof(echo)-1,0,nullptr,nullptr);//不关心服务器的地址
if(ret > 0)
{
echo[ret]='\\0';
cout<<"客户端受到回应"<< echo<< endl;
}
}
}
~udpClient()
{
close(_sock);
}
};
udpClient.cpp
#include "udpClient.h"
int main(int argc,char* argv[])
{
if(argc!=3)
{
cout<<"服务器地址没有传入"<<endl;
exit(1);
}
udpClient uu(argv[1],atoi(argv[2]));
uu.initClient();//初始化客户端
uu.startClient();//启动客户端
}
(3)效果
使用netstat nlup
可以查看网络进程信息
1:本地环回测试
2:局域网IP
3:公网IP
以上是关于2-3:套接字(Socket)编程之UDP通信,sockaddr,sockaddr_in,recvfrom,sendto的主要内容,如果未能解决你的问题,请参考以下文章