目录
[一. Socket编程API与结构体](#一. Socket编程API与结构体)
[1.1 Socketaddr结构体](#1.1 Socketaddr结构体)
[二. UDP网络编程API⭐](#二. UDP网络编程API⭐)
[2.1 socket](#2.1 socket)
[2.2 bind](#2.2 bind)
[2.3 sendto](#2.3 sendto)
[2.4 recvfrom](#2.4 recvfrom)
[三. UDP服务端代码](#三. UDP服务端代码)
[3.1 udpserver.hpp](#3.1 udpserver.hpp)
[3.2 udpserver.cc](#3.2 udpserver.cc)
[四. UDP客户端代码](#四. UDP客户端代码)
[4.1 udpclient.hpp](#4.1 udpclient.hpp)
[4.2 udpclient.cc](#4.2 udpclient.cc)
[五. 测试](#五. 测试)
一. Socket编程API与结构体
socket是插座的意思,插就是应用程序,座就是网络socket。通信双方都通过socket进行通信。
1.1 Socketaddr结构体
有三套结构体,sockaddr sockaddr_in sockaddr_un
sockaddr:原始套接字,可以通过传输层控制底层结构,常用于网络中的监听,抓包等功能。结构如下:

sockaddr_in:网络套接字,常用于跨主机之间的网络通信。内部**集合了主机的IP和端口,在大部分网络编程API中,需要强制转化为原始套接字。**结构如下:

Unix域间套接字:用于主机内部的通信,类似于管道和共享内存。结构如下:

在使用这些结构体的时候,通过将 sockaddr_in sockaddr_un转化为 sockaddr即可实现相应类型的通信。
其实就是一种多态的思想,soackaddr就是基类,剩下两种是派生类。
二. UDP网络编程API⭐
UDP的特点是不保证可靠传输,面向数据报,无连接。
所以:
我们每一次接收数据的时候,应该将一个完整的报文读取完毕。
由于无连接,发送数据的时候需要带上自己的ip和端口等信息
收数据的时候也要定义对象来获取对方的ip和端口等信息
2.1 socket
cpp
//头文件
#include <sys/types.h>
#inclyde <sys/socket.h>
//函数原型,用于创建一个用于网络通信的套接字fd
int socket(int domin, int type, int protocol)
//参数说明
//domin : 域,表示是需要网络通信还是主机内部通信
//type : 通信的类型
//SOCK_STREAM 是流式套接字通信,即TCP通信
//SOCK_DGRAM 是报文套接字通信,即UDP通信
// protocol,
// 分为tcpprotocol udpprotoclo 但是我们一般设置为0,因为type参数选择之后,这个就会自动固定
//返回值,返回一个用于通信的文件fd,其实socket与open是非常类似的
Linux中一切皆文件,打开网络与打开普通文件是一样的。读写网络数据与读写文件数据也是非常类似的
2.2 bind
用于绑定sockfd与IP端口
cpp
#include <sys/types.h>
#inckude <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
sockfd : 使用socket获取的文件描述符
addr : 传入的结构体,该结构体是你需要通信的方式,内部包含了IP和端口等信息
addrlen : 传入结构体的长度,需要注意类型
返回值:绑定成功返回0,失败返回-1
//注意,我们定义下面三种结构体的时候需要带上头文件#include <arpa/inet.h>
struct sockaddr s;
s.sin_family = AF_INET 表示协议家族,我们需要传入协议,如AF_INET表示使用网络通信
s.sin_port 表示绑定端口号
s.sin_addr 存储了IP号码
s.sin_addr.s_addr 即可获取IP地址
2.3 sendto
这个接口常用于UDP服务器/客户端之间的发送信息
cpp
#include <sys/types.h>
#include <sys/socket.h>
用于发送信息到网络中
//函数原型
ssize_t sendto(int sockfd, void *buf, size_t len, int flags, struct sockaddr* dest_addr, socklen_t *addrlen);
//将 len长度的buf数据通过sockfd这个套接字发送到网络中
flags为0表示阻塞
//输入形参数
dest_addr 对方接收数据的IP地址和端口
addrlen 对方接收数据结构体长度
2.4 recvfrom
用于UDP通信时候接收数据
cpp
#include <sys/types.h>
#include <sys/socket.h>
用于接收来自网络的信息
//函数原型
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t *addrlen)
//参数
sockfd 接收信息的文件描述符
bur 接收信息写入的缓冲区 len 接收信息的长度
flags 接收信息的方式,一般默认设置为0 表示阻塞方式,没有信息阻塞等待
//下面两个参数是输入输出形参数,是为了获取对方的IP信息与端口号码,协议方式
src_addr 对方IP的信息
addrlen 该信息的长度,注意这个参数在定义的时候不需要初始化为src_addr的长度 addrlen = sizeof(src_addr)
三. UDP服务端代码
这个服务器用于接收客户端的代码,然后打印输出。
3.1 udpserver.hpp
服务器的代码框架如下:
cpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
static const std::string defaultIp = "0.0.0.0";
class udpServer
{
public:
udpServer(uint16_t port, const std::string &ip = defaultIp)
: _sockfd(-1), _ip(ip), _port(port) {}
void init() {}
void run() {}
private:
int _sockfd; // 套接字
std::string _ip; // 服务器ip
uint16_t _port; // 绑定的端口
};
需要变量来存储服务器的 通信fd,ip和端口。
init用于初始化服务器:包含socket,bind
run:用于服务器的运行,一个服务器一般是一个死循环执行代码的。
一般来说,一台主机是有很多ip的,为了能够保证接收数据不丢失,服务器绑定的IP一般都是"0.0.0.0"表示服务器可以接收底层任意ip地址的网络数据
init代码:
cpp
void init()
{
// 1.socket创建套接字,udp需要使用 报文
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
perror("socket");
exit(errno);
}
printf("socket success\n");
// 2.绑定fd和端口
// 首先要设置好通信结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(_ip.c_str()); // 需要转化为大端数据
serveraddr.sin_port = htons(_port); // 需要转化为大端数据
socklen_t len = sizeof(serveraddr); // 设置bind第三个参数
int n = bind(_sockfd, (struct sockaddr *)&serveraddr, len);
if (n < 0)
{
perror("bind");
exit(errno);
}
printf("bind success\n");
// UDP在绑定好之后,就能运行了
}
run:
服务器运行的时间是很长的,所以我们需要使用一个死循环来读取数据。
cpp
void run()
{
char buffer[1024];
while (true)
{
// 收数据,要定义对象获取对方的ip和端口
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t clientaddrlen = sizeof(clientaddr);
// recvfrom接收数据
int count = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&clientaddr, &clientaddrlen);
if (count > 0)
{
// 通过设置好的sockaddr获取ip,端口等信息
std::string clientip = inet_ntoa(clientaddr.sin_addr);
uint16_t clientport = ntohs(clientaddr.sin_port);
// 打印收到数据的信息
printf("client[%s][%d] --> server:%s\n", clientip.c_str(), clientport, buffer);
}
// 清空 buffer
memset(buffer, 0, sizeof(buffer));
}
}
总代码如下:
cpp
namespace YZC
{
static const std::string defaultIp = "0.0.0.0";
class udpServer
{
public:
udpServer(uint16_t port, const std::string &ip = defaultIp)
: _sockfd(-1), _ip(ip), _port(port) {}
void init()
{
// 1.socket创建套接字,udp需要使用 报文
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
perror("socket");
exit(errno);
}
printf("socket success\n");
// 2.绑定fd和端口
// 首先要设置好通信结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(_ip.c_str()); // 需要转化为大端数据
serveraddr.sin_port = htons(_port); // 需要转化为大端数据
socklen_t len = sizeof(serveraddr); // 设置bind第三个参数
int n = bind(_sockfd, (struct sockaddr *)&serveraddr, len);
if (n < 0)
{
perror("bind");
exit(errno);
}
printf("bind success\n");
// UDP在绑定好之后,就能运行了
}
void run()
{
char buffer[1024];
while (true)
{
// 收数据,要定义对象获取对方的ip和端口
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t clientaddrlen = sizeof(clientaddr);
// recvfrom接收数据
int count = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&clientaddr, &clientaddrlen);
if (count > 0)
{
// 通过设置好的sockaddr获取ip,端口等信息
std::string clientip = inet_ntoa(clientaddr.sin_addr);
uint16_t clientport = ntohs(clientaddr.sin_port);
// 打印收到数据的信息
printf("client[%s][%d] --> server:%s\n", clientip.c_str(), clientport, buffer);
}
// 清空 buffer
memset(buffer, 0, sizeof(buffer));
}
}
private:
int _sockfd; // 套接字
std::string _ip; // 服务器ip
uint16_t _port; // 绑定的端口
};
}
3.2 udpserver.cc
这个代码包含main函数,用于服务器的启动和测试
cpp
#include <iostream>
#include <memory>
#include "udpserver.hpp"
// 设置使用方法
void Usage(const char *s)
{
printf("Usage:\r\n%s serverport\n", s);
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(-1);
}
// 使用智能指针管理服务器,可以自动销毁。防止内存泄漏
std::unique_ptr<YZC::udpServer> usptr(new YZC::udpServer(atoi(argv[1])));
usptr->init();
usptr->run();
return 0;
}
四. UDP客户端代码
这个客户端用于向服务端发送数据
思考一下:服务端为何需要bind绑定fd和端口?
因为,如果没有指明端口,其他客户端如何连接服务器?
思考一下:客户端需要显示绑定fd和端口吗?
不需要,因为通信是由客户端发起的,服务端被动接收。此时服务端无需知道客户端具体的ip和端口信息
服务端可以直接通过recvfrom获取我的ip/端口等信息
4.1 udpclient.hpp
框架与server类似
cpp
namespace YZC
{
class udpClient
{
public:
udpClient(std::string &ip, uint16_t port)
: _ip(ip), _port(port) {}
void init()
{
}
void start()
{
}
private:
int _sockfd; // 监听套接字
std::string _ip; // 目的ip
uint16_t _port; // 目的端口
};
}
init : 上面讨论过了,只需要socket,无需显示bind。如果没有bind,在发送数据的时候内核会自动帮我们找到没有使用的port进行bind
cpp
void init()
{
// 1.socket创建套接字,udp需要使用 报文
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
perror("socket");
exit(errno);
}
printf("client socket success\n");
// bind由OS自行决定
}
start:
用于发送数据
cpp
void start()
{
// 发送数据,需要指定对方的ip和端口
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
// 设置ip/端口/通信等信息。注意转化为大端数据
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(_ip.c_str());
serveraddr.sin_port = htons(_port);
socklen_t clientaddrlen = sizeof(serveraddr);
// 可以发送数据
char buffer[1024];
while (true)
{
memset(buffer, 0, sizeof(buffer));
std::cout << "请输入你想要向server发送的数据;";
std::cin >> buffer;
int count = sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&serveraddr, clientaddrlen);
if (count <= 0)
{
printf("数据发送异常,client quit\n");
close(_sockfd);
exit(-1);
}
printf("数据发送成功\n");
}
}
总代码如下:
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
namespace YZC
{
class udpClient
{
public:
udpClient(std::string &ip, uint16_t port)
: _ip(ip), _port(port) {}
void init()
{
// 1.socket创建套接字,udp需要使用 报文
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
perror("socket");
exit(errno);
}
printf("client socket success\n");
// bind由OS自行决定
}
void start()
{
// 发送数据,需要指定对方的ip和端口
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
// 设置ip/端口/通信等信息。注意转化为大端数据
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(_ip.c_str());
serveraddr.sin_port = htons(_port);
socklen_t clientaddrlen = sizeof(serveraddr);
// 可以发送数据
char buffer[1024];
while (true)
{
memset(buffer, 0, sizeof(buffer));
std::cout << "请输入你想要向server发送的数据;";
std::cin >> buffer;
int count = sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&serveraddr, clientaddrlen);
if (count <= 0)
{
printf("数据发送异常,client quit\n");
close(_sockfd);
exit(-1);
}
printf("数据发送成功\n");
}
}
private:
int _sockfd; // 监听套接字
std::string _ip; // ip
uint16_t _port; // 绑定的端口
};
}
4.2 udpclient.cc
cpp
#include <iostream>
#include <memory>
#include "udpclient.hpp"
// 设置使用方法
void Usage(const char *s)
{
printf("Usage:\r\n%s serverip serverport\n", s);
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(-1);
}
// 使用智能指针管理服务器,可以自动销毁。防止内存泄漏
std::unique_ptr<YZC::udpClient> ucptr(new YZC::udpClient(argv[1], atoi(argv[2])));
ucptr->init();
ucptr->start();
return 0;
}
五. 测试
使用上面的代码来测试服务端server与客户端client 能否通信
这里首先了解一下本地回环地址 127.0.0.1。使用命令 ifconfig查看网卡信息
下面的数据是我在自己的云服务器输入ifconfig获取的。
cpp
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.52.126 netmask 255.255.192.0 broadcast 172.17.63.255
inet6 fe80::216:3eff:fe0a:5e1e prefixlen 64 scopeid 0x20<link>
ether 00:16:3e:0a:5e:1e txqueuelen 1000 (Ethernet)
RX packets 1944787 bytes 1196104294 (1.1 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1537251 bytes 634615558 (605.2 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 746248 bytes 164466741 (156.8 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 746248 bytes 164466741 (156.8 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
可以看第二段的第二行,本地回环地址是 127.0.0.1
首先要启动服务器

然后启动客户端

运行结果如下:

可以看到,我们的数据被客户端正常发送了。服务端也接收到来正确的数据
这样一来就能保证 server - client之间的通信,当然这份代码是比较简单的。
