其实我们听TCP, UDP听得很多了, 想不想自己实现一个简易的UDP通信呢?
那么我们这次来学习网络socket编程, 话不多说, 直接开整
文章目录
1. UDP通信流程
UDP – 用户数据报协议
无连接, 不可靠, 面向数据报
udp协议用于实时性要求大于安全性的场景 — 视频/音频数据传输
了解客户端和服务端的概念
客户端: 主动发起请求的一方 (一般是用户)
服务端: 被动接收请求的一方 (向用户提供服务的一方)
下面给出UDP通信中客户端和服务端的各自的流程
2. socket接口
头文件<sys/socket.h>
创建套接字
int socket(int domain, int type, int protocol);
domain: 地址域类型 -- AF_INET (ipv4地址域)
type: 套接字类型 -- SOCK_STREAM 字节流服务 / SOCK_DGRAM数据报服务
protocol: 协议类型, 0 默认类型/ IPPROTO_TCP/ IPPROTO_UDP
字节流服务默认TCP协议, 数据报服务默认是UDP服务
返回值: 成功返回一个非负整数 -- 套接字描述符, 失败返回-1
为套接字绑定地址信息
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 创建套接字返回的操作句柄
addr: struct socketaddr结构体, 不同的地址域有对应不同的地址结构
addrlen: 实际地址结构的长度
返回值: 成功返回0, 失败返回-1
发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd: 套接字操作句柄, buf: 要发送数据的空间首地址 len: 数据长度
flags: 标志位, 默认给0--阻塞发送, dest_addr: 接收端地址信息 addrlen: 地址信息长度
返回值: 成功返回实际发送数据的字节长度, 失败返回-1.
接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
sockfd: 套接字操作句柄, buf: 放置数据的缓冲区空间首地址,
len: 要接收的数据长度 (不能大于上面缓冲区长度)
flags: 默认0 -- 阻塞接收(有数据取, 没数据等待) src_addr: 发送端的地址信息
socklen_t *addrlen 输入输出型参数 -- 指定想要的地址长度, 返回实际的地址长度
返回值: 成功返回实际接收的数据长度, 失败返回-1
关闭套接字
int close(sockfd);
地址转换接口
htons/htonl: 主机字节序到网络字节序的整数转换 short-16/int-32
ntohs/ntohl: 网络字节序到主机字节序的整数转换 short-16/int-32
点分十进制字符串IP地址到网络字节序IP地址的转换
in_addr_t inet_addr(const char *cp);
网络字节序IP地址到点分十进制字符串IP地址的转换
char *inet_ntoa(struct in_addr in);
int inet_pton(int af, const char *src, void *dst);
af: 地址域类型 -- AF_INET, src: 字符串IP地址,
dst: 返回转换后的整数地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af: 地址域类型, src: 网络字节序整数IP,
dst: 返回转换后的字符串, size: dst空间长度
看完接口之后我们就要实现了, 大家要把接口好好理解, 否则很容易搞乱~
3. 代码实现UDP通信
封装socket接口
创建udpsocket.hpp头文件, 进行接口的封装
#include <iostream>
#include <string>
#include <unistd.h>
#include <arpa/inet.h> //地址转换接口头文件
#include <netinet/in.h> // 地址结构类型定义头文件
#include <sys/socket.h> //套接字接口头文件
using namespace std;
class UdpSocket {
public:
UdpSocket():_socket(-1) {}
//创建套接字
bool Socket() {
//int socket(int domain, int type, int protocol);
_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(_socket < 0) {
perror("socket error");
return false;
}
return true;
}
//为套接字绑定地址信息
bool Bind(const string& ip, int port) {
//定义ipv4地址结构 struct socketaddr_in
struct sockaddr_in addr;
addr.sin_family = AF_INET;
//htons:将主机字节序短整型数据转换为网络字节序数据
addr.sin_port = htons(port);
//将字符串IP地址转换为网络字节序
addr.sin_addr.s_addr = inet_addr(ip.c_str());
//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_socket, (struct sockaddr*)&addr, len);
if(ret < 0) {
perror("bind error");
return false;
}
return true;
}
bool Send(string& data, string& ip, int port) {
//sento(套接字句柄,数据首地址,数据长度,标志位,对端地址信息,地址信息长度)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htos(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = sendto(_socket, data.c_str(), data.size(), 0, (struct sockaddr*)&addr, len);
if(ret < 0) {
perror("sendto error");
return false;
}
return true;
}
//接收数据,获取发送端的地址信息
bool Recv(string* buf, string* ip = nullptr, uint16_t* port = nullptr) {
//recvfrom(套接字句柄,接收缓冲区,数据长度,标志,源端地址,地址长度)
struct sockaddr_in peer_addr;
socklen_t len = sizeof(struct sockaddr_in);
char tmp[4096] = {0};
int ret = recvfrom(_socket, tmp, 4096, 0, (struct sockaddr*)&peer_addr, &len);
if(ret < 0) {
perror("recvfrom error");
return false;
}
//从指定字符串中截取指定长度的数据到buf中
buf->assign(tmp, ret);
if(port != nullptr) {
//网络字节序到主机字节序的转换
*port = ntohs(peer_addr.sin_port);
}
if(ip != nullptr) {
//网络字节序到字符串IP地址的转换
*ip = inet_ntoa(peer_addr.sin_addr);
}
return true;
}
//关闭套接字
bool Close() {
if(_socket > 0) {
close(_socket);
_socket = -1;
}
return true;
}
private:
int _socket;
};
OK, 这样我们就封装好了一个UDP协议的接口, 接下来我们只要根据流程调用接口即可.
模拟实现服务端
创建udp_server.cpp文件, 进行服务端的实现
#include <iostream>
#include <string>
#include "udpsocket.hpp"
using namespace std;
//#define CHECK_RET(q) if((q)==false) {return false;}
int main(int argc, char* argv[]) {
//argc表示程序运行参数的个数
if(argc != 3) {
cout << "Usage: ./udp_server ip prot" << endl;
return -1;
}
uint16_t port = stoi(argv[2]);
string ip = argv[1];
UdpSocket srv_sock;
//创建套接字
if (srv_sock.Socket() == false) {
return -1;
}
//绑定地址信息
if (srv_sock.Bind(ip, port) == false) {
return -1;
}
while(1) {
//接收数据
string buf;
string peer_ip;
uint16_t peer_port;
if (srv_sock.Recv(&buf, &peer_ip, &peer_port) == false) {
break;
}
cout << "client[" << peer_ip << ":" << peer_port << "] say: " << buf << endl;
//回复数据
string data;
cout << "server say: ";
cin >> data;
if(srv_sock.Send(data, peer_ip, peer_port) == false) {
break;
}
}
//关闭套接字
srv_sock.Close();
return 0;
}
模拟实现客户端
创建udp_client.cpp文件, 进行服务端的实现
#include <iostream>
#include <string>
#include "udpsocket.hpp"
using namespace std;
int main(int argc, char* argv[]) {
//客户端参数获取的ip地址是服务端绑定的地址, 也就是客户端发送的目标地址
//不是为了自己绑定的
if (argc != 3) {
cout << "Usage: ./udp_cli ip port" << endl;
return -1;
}
string srv_ip = argv[1];
uint16_t srv_port = stoi(argv[2]);
UdpSocket cli_sock;
//创建套接字
if(cli_sock.Socket() == false) {
return -1;
}
//绑定地址(不推荐)
while(1) {
//发送数据
cout << "client say:";
string data;
cin >> data;
if (cli_sock.Send(data, srv_ip, srv_port) == false) {
return -1;
}
//接收数据
string buf;
//客户端不需要关心服务器的ip和port信息, 只需要接收数据就行了
if (cli_sock.Recv(&buf) == false) {
return -1;
}
cout << "server say: " << buf << endl;
}
//关闭套接字
cli_sock.Close();
return 0;
}
OK, 到这里我们就实现完毕啦, 下面就可以编译测试了~
makefile文件编写
all:udp_client udp_server
udp_client:udp_client.cpp
g++ -std=c++11 $^ -o $@
udp_server:udp_server.cpp
g++ -std=c++11 $^ -o $@
编译后开两个渠道, 一个运行服务端, 一个运行客户端, 就可以开始对话啦~