客户端、服务器端
网络通信是两端主机的通信,通信两端被分为:客户端,服务器端。
客户端:发送请求的一端,主动发起请求的一端
服务器端:提供服务的一端,被动接收请求的一端
UDP协议
UDP协议 :用户数据报协议
特点:无连接,不可靠,面向数据报
应用场景:传输实时性要求高于安全性要求的场景---视频传输
UDP的通信流程
因为UDP是无连接的,所以只要创建套接字绑定地址信息,就可以进行通信。
客户端一般不会主动绑定地址信息。 如果不绑定地址信息,系统会选择合适的地址信息进行绑定,若我们手动绑定,可能会绑定到已经使用的端口,这样就会发生端口冲突。还有如果在客户端代码中绑定地址信息,第一个客户端会绑定成功,因为客户端不止一个,当第二个客户端绑定信息时就会失败,因为第一个客户端已经绑定地址和端口,所以就不会主动绑定地址信息。
Socket编程
Sockaddr
struct sockaddr是通用的套接字地址,根据所使用的协议不同,选择不同的结构,通信时再将我们所使用的结构强转为sockaddr*,这样就能保证数据格式的一致。
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
- IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API可以都用struct sockaddr *类型表示, 在使用的时候需要将具体类型强制转化成struct sockaddr *; 这样的好 处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
通常我们使用IPv4,所以一般使用sockaddr_in结构体,这个结构体主要描述了ip地址和端口。
sockaddr_in结构体:
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
sin_port存储端口号(使用网络字节顺序)
sin_addr存储IP地址,sin_addr是一个结构体,结构体中只有一个成员s_addr
sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
字节序
主机字节序(由cpu架构决定):一台主机上cpu对内存中数据按照字节为单位进行存取的顺序,也就是通常说的大端小端问题。
若两通信主机字节序不同,则有可能在网络通信中会产生数据二义,想要避免因为主机字节序不同而导致的数据二义,则需要在网络中统一字节序标准,就是网络字节序,网络字节序是大端字节序,也就意味着如果你的主机是小端,则在网络通信时需要将数据转换为网络字节序后进行发送。
在arpa/inet.h
这个头文件中,也为我们提供了一套字节序的转换接口。
uint32_t htonl(uint32_t hostlong);32位数据主机到网络字节序转换
uint16_t htons(uint16_t hostshort);16位数据主机到网络字节序转换
uint32_t ntohl(uint32_t netlong);32位数据网络到主机字节序转换
uint16_t ntohs(uint16_t netshort);16位数据网络到主机字节序转换
port端口转换使用uint16_t类型,ip地址转换使用uint32_t,两者不能混用。
地址转换
通常我们输入的ip地址一般都是点分十进制的,但网络通信需要的是网络字节序的整数ip地址。
#include <arpa/inet.h>
int_addr_t inet_addr(const char* cp); 将字符串点分十进制IP地址转换为整形网络字节序IP地址
char* inet_ntoa(struct int_addr in); 将网络字节序IP地址转换为字符串点分十进制IP地址
socket相关接口
1.创建套接字
int socket(int domain, int type, int protocol);
domain:地址域类型 AF_INET-IPv4通信,使用IPv4的地址结构
type:套接字类型; SOCK_STREAM 字节流 /SOCK_DGRAM 数据报
udp通信使用SOCK_DGRAM
protocol:本次通信协议 IPPROTO_TCP-6/IPPROTO_UDP-17
返回值:返回一个文件描述符-操作句柄 失败返回-1
2.为套接字绑定地址信息
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen)
sockfd:创建套接字返回的操作句柄
addr:当前绑定的地址信息
socklen_t:地址信息长度
返回值:成功返回0,失败返回-1
3.接收数据
ssize_t recvfrom(int sockfd, void* buf, int len, int flag,
struct sockaddr* srcaddr, socklen_t *addrlen)
sockfd:操作句柄
buf:空间地址,用于存放接收的数据
len:要接收的数据长度
flag:选项标志---默认0--表示阻塞接收
srcaddr:本条数据的源端地址信息
addrlen:输入输出参数-指定要接收多长的地址结构,并且返回实际接收的地址长度
返回值:返回实际接收到的数据长度;失败返回-1
4.发送数据
ssize_t sendto(int sockfd, void* data, int len, int flag,
struct sockaddr* peeraddr, socklen_t addrlen)
sockfd:操作句柄
data:要发送的数据的空间首地址
len:要发送的数据长度
flag:默认0---阻塞发送
peeraddr:对端地址信息
addrlen:地址结构长度
返回值:成功返回实际发送的数据长度,失败返回-1;
5.关闭套接字
int close(int sockfd);
UDPSocket封装
为了使用更加方便,我们对UDP进行了封装,使用的时候,调用接口即可。
#include<iostream>
using namespace std;
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<cstdio>
#include<netinet/in.h>
#define CHECK(p) if(p == false) {return -1;}
class UDPSocket
{
public:
UDPSocket()
:_sockfd(-1)
{}
//创建套接字
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_DGRAM,IPPROTO_UDP);
if(_sockfd < 0)
{
perror("create socket error");
return false;
}
return true;
}
//绑定信息
bool Bind(const string& ip, const uint16_t& port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
//主机字节序转换为网络字节序
addr.sin_port = htons(port);
//将字符串的点分十进制ip地址转换为网络字节序的整数形式ip地址
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int len = sizeof(addr);
int ret = bind(_sockfd, (sockaddr*)&addr,len);
if(ret < 0)
{
perror("bind error");
return false;
}
return true;
}
//发送数据
bool Send(string& data, const string& ip, const uint16_t& port)
{
sockaddr_in peeraddr;
peeraddr.sin_family = AF_INET;
peeraddr.sin_port = htons(port);
peeraddr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(sockaddr_in);
int ret = sendto(_sockfd, data.c_str(), data.size(), 0, (sockaddr*)&peeraddr, len);
if(ret < 0)
{
perror("send error");
return false;
}
return true;
}
//接收数据
bool Recv(string& data, string* ip = NULL, int* port = NULL)
{
char tmp[4096] = {0};
sockaddr_in srcaddr;
socklen_t len = sizeof(srcaddr);
int ret = recvfrom(_sockfd, tmp, 4095, 0, (sockaddr*)&srcaddr,&len);
if(ret < 0)
{
perror("recv error");
return false;
}
data.assign(tmp, ret);
if(port != NULL)
*port = ntohs(srcaddr.sin_port);
if(ip != NULL)
*ip = inet_ntoa(srcaddr.sin_addr);
return true;
}
//关闭套接字
bool Close()
{
if(_sockfd != -1)
{
close(_sockfd);
_sockfd = -1;
}
return true;
}
private:
int _sockfd;
};
UDP服务器
根据服务器的流程,调用其接口
1.创建套接字 socket()
2.绑定地址信息 bind()
3.接收数据 recvfrom()
4.发送数据 sendto()
5.关闭套接字 close()
#include"udp.hpp"
int main()
{
UDPSocket sock;
//创建套接字
CHECK(sock.Socket());
//绑定地址信息
CHECK(sock.Bind("192.168.134.141",9000 ));
while(1)
{
//接收数据
string data;
string ip;
int port;
bool ret = sock.Recv(data, &ip, &port);
if(ret == false)
continue;
cout << "cli_ip:" << ip <<" port:" << port << " say:"<< data<< endl;
//发送数据
data.clear();
cout << "serve say:";
getline(cin, data);
ret = sock.Send(data, ip, port);
if(ret == false)
{
cout << "serve say failed" << endl;
}
}
//关闭套接字
sock.Close();
return 0;
}
UDP客户端
1.创建套接字 socket()
2.绑定地址信息(不推荐) bind()
3.发送数据 sendto()
4.接收数据 recvfrom()
5.关闭套接字 close()
#include"udp.hpp"
int main()
{
UDPSocket sock_cli;
//创建套接字
CHECK(sock_cli.Socket());
//绑定地址信息(不推荐)
while(1)
{
//发送数据
string buf;
cout << "client say:";
getline(cin, buf);
CHECK(sock_cli.Send(buf, "192.168.134.141", 9000));
buf.clear();
//接收数据
CHECK(sock_cli.Recv(buf));
cout << "server say:" << buf << endl;
}
//关闭套接字
sock_cli.Close();
return 0;
}
示例:
服务端:
客户端1:
客户端2: