目录
udp
预备知识
了解sockaddr_in
struct sockaddr_in {
sa_family_t sin_family; // 地址族,对于IPv4,总是AF_INET
uint16_t sin_port; // 端口号,网络字节序
struct in_addr sin_addr; // IPv4地址
// 注意:在某些实现中,可能还有额外的填充字节以确保结构体的对齐
};
// 其中,struct in_addr 是一个小结构体,通常只包含一个32位的IPv4地址
struct in_addr {
uint32_t s_addr; // IPv4地址,网络字节序
};
网络字节顺序转主机字节顺序(大小端问题)
在网络通信中,由于不同的计算机系统可能采用不同的字节序(字节在内存中的排列顺序)来存储数据,因此在数据交换时需要进行字节序的转换。这主要涉及到网络字节序和主机字节序之间的转换。网络字节序通常采用大端模式,而主机字节序则可能是大端模式或小端模式,具体取决于计算机系统的架构。
简单接口
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
其中“h”代表host(主机),“n”代表network(网络),“to”表示转换方向,“s”代表short(短整型),“l”代表long(长整型)。
常用函数介绍
UDP是一种无连接的协议,与TCP不同,它不提供错误恢复、重传机制或顺序保证。
以下是UDP连接(更准确地说,是UDP通信的初始化过程,因为UDP是无连接的)时所用的主要函数:
socket():
int socket(int domain, int type, int protocol);
- 作用:创建一个新的套接字。对于UDP,
domain
参数通常是AF_INET
(IPv4)或AF_INET6
(IPv6),type
参数是SOCK_DGRAM
表示数据报套接字,protocol
通常是0,让系统自动选择UDP协议。 - 参数意义:
- domain:指定套接字使用的协议族。对于IPv4网络,此参数通常是
AF_INET
;对于IPv6网络,则是AF_INET6
。 - type:指定套接字的类型。对于UDP,此参数是
SOCK_DGRAM
,表示数据报套接字。 - protocol:指定具体的协议。通常,对于UDP来说,这个参数设置为0,因为
SOCK_DGRAM
已经隐含了UDP协议。
- domain:指定套接字使用的协议族。对于IPv4网络,此参数通常是
- 返回值:成功时返回一个新的套接字描述符(非负整数),失败时返回-1并设置errno以指示错误
bind():
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 作用:将套接字与特定的IP地址和端口号绑定。对于UDP服务器,这一步是必要的,因为它需要监听一个特定的端口以接收客户端的消息。对于UDP客户端,如果不需要在特定端口上接收数据,也可以不调用
bind()
。 - 参数意义:
- sockfd:通过
socket()
函数获得的套接字描述符。 - addr:指向
sockaddr
结构的指针,该结构包含了套接字将要绑定的IP地址和端口号。对于IPv4,通常使用sockaddr_in
结构。 - addrlen:
addr
参数指向的缓冲区的大小,以字节为单位。
- sockfd:通过
- 返回值:成功时返回0,失败时返回-1并设置errno以指示错误。
sendto()
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
- 作用:
sendto()
用于发送UDP数据报 - 参数意义:
- sockfd:要发送数据的套接字描述符。
- buf:指向要发送数据的缓冲区的指针。
- len:缓冲区中数据的长度。
- flags:通常设置为0,表示没有特殊标志。
- dest_addr:指向
sockaddr
结构的指针,该结构包含了目标地址和端口号。 - addrlen:
dest_addr
参数指向的缓冲区的大小,以字节为单位。
- 返回值:成功时返回发送的字节数,失败时返回-1并设置errno以指示错误。
recvfrom():
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- 作用:
recvfrom()
用于接收UDP数据报。 - 参数意义:
- sockfd:要接收数据的套接字描述符。
- buf:指向接收数据的缓冲区的指针。
- len:缓冲区的大小,即可以接收的最大字节数。
- flags:通常设置为0,表示没有特殊标志。
- src_addr:可选参数,如果非NULL,则接收发送方的地址信息。
- addrlen:输入时表示
src_addr
缓冲区的大小,输出时表示实际接收到的地址信息的长度。
- 返回值:成功时返回接收到的字节数,如果连接被对方正常关闭,则返回0;失败时返回-1并设置errno以指示错误。但请注意,对于UDP来说,
recvfrom()
返回0通常表示超时或某些特定条件下的错误,而不是正常的连接关闭。
close():
int close(int fd);
- 作用:关闭套接字。完成UDP通信后,应关闭套接字以释放资源。
-
参数意义: fd:要关闭的文件描述符,对于套接字编程来说,就是套接字描述符。
-
返回值:成功时返回0,失败时返回-1并设置errno以指示错误。
请注意,UDP编程不需要像TCP那样进行显式的“连接”和“断开连接”操作。相反,UDP应用通过 sendto()
和 recvfrom()
直接发送和接收数据报。这些函数允许UDP应用在没有任何预先建立连接的情况下,向任何IP地址和端口发送数据报,并从任何IP地址和端口接收数据报。
实践(一个回显打印服务)
大致流程
UDP通信的大致流程可以分为服务器端和客户端两部分:
服务器端流程
- 创建套接字(Socket)
- 绑定地址和端口
- 接收数据
- 处理数据
- 发送数据(可选)
- 关闭套接字
客户端流程
- 创建套接字(Socket)
- (可选)绑定地址和端口
- 发送数据
- 接收数据(可选
- 关闭套接字
ServerMain.cpp 服务器的主程序
#include "UdpServe.h"
#include <memory>
int main(int agrc, char *argv[])
{
if (agrc != 2)
{
std::cerr << "usage: " << argv[0] << " local port" << std::endl;
exit(0);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<Udpserver> usvr = std::make_unique<Udpserver>(port);
usvr->Initserver();
usvr->start();
return 0;
}
Udpserve.h 对udp操作封装
static const int gscokfd = -1;
static const uint16_t glocalpoart = 8888;
enum
{
SOCEKFD_ERROT = -1,
BIND_ERROT
};
class Udpserver : public nocopy
{
public:
Udpserver(uint16_t localport = glocalpoart)
: _sockfd(gscokfd)
, _localPort(localport)
, isrunning(false)
{
}
void Initserver()
{
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
std::cout << "socket error" << std::endl;
exit(SOCEKFD_ERROT);
}
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_localPort);
local.sin_addr.s_addr = INADDR_ANY; // 服务器端进行任意ip绑定
int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
std::cout << "bind error" << std::endl;
exit(BIND_ERROT);
}
}
void start()
{
isrunning = true;
char buffer[1024];
while (isrunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
InterAddr addr(peer);
buffer[n] = 0;
std::cout << "[" << addr.ip << ":" << addr.port << "]#" << buffer << std::endl;
std::string echostr = "udp server#";
echostr += buffer;
sendto(_sockfd, echostr.c_str(), echostr.size(), 0, (struct sockaddr *)&peer, len);
}
else
{
std::cout << "recvfrom , error" << std::endl;
}
}
}
~Udpserver()
{
if (_sockfd > gscokfd)
::close(_sockfd);
}
private:
int _sockfd;
uint16_t _localPort;
bool isrunning;
};
InterAddr.h 进行转换(主机大小端不确定)
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class InterAddr
{
private:
void ToHost(const struct sockaddr_in &addr)
{
port = ntohl(addr.sin_port);
ip = inet_ntoa(addr.sin_addr);
}
public:
std::string ip;
uint16_t port;
struct sockaddr_in _addr;
public:
InterAddr() {}
InterAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
ToHost(addr);
}
bool operator==(const InterAddr &t)
{
return (this->ip == t.ip && this->port == t.port);
}
std::string Ip()
{
return ip;
}
uint16_t Port()
{
return port;
}
struct sockaddr_in Addr()
{
return _addr;
}
std::string AddrStr()
{
return ip + ":" + std::to_string(port);
}
~InterAddr()
{
}
};
ClientMain.cpp 客户端
#include<iostream>
#include <string>
#include<bits/stdc++.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket errot" << std::endl;
exit(0);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(serverport);
local.sin_addr.s_addr = inet_addr(serverip.c_str());
while (1)
{
std::string line;
std::cout << "请输入:" << std::endl;
std::getline(std::cin, line);
int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&local, sizeof(local));
if (n > 0)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
else
{
std::cerr << "recvform error" << std:: endl;
exit(0);
}
}
else
{
std::cerr << "sendto error" << std::endl;
exit(0);
}
}
close(sockfd);
return 0;
}