文章目录
目录
[1. 创建套接字 - socket 函数](#1. 创建套接字 - socket 函数)
[2. 绑定地址和端口 - bind 函数](#2. 绑定地址和端口 - bind 函数)
[3. 发送数据 - sendto 函数](#3. 发送数据 - sendto 函数)
[4. 接收数据 - recvfrom 函数](#4. 接收数据 - recvfrom 函数)
[5. 关闭套接字 - close 函数](#5. 关闭套接字 - close 函数)
前言
一、upd函数及接口介绍
1. 创建套接字 - socket
函数
cpp
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- 参数说明:
domain
:指定协议族,对于 UDP 编程,通常使用AF_INET
表示 IPv4 协议族。type
:指定套接字类型,对于 UDP,使用SOCK_DGRAM
表示数据报套接字。protocol
:指定具体的协议,一般设置为 0,让系统自动选择合适的协议(对于SOCK_DGRAM
通常为 UDP)。
- 返回值:
- 成功时返回一个非负的套接字描述符;失败时返回 -1,并设置
errno
以指示错误类型。
- 成功时返回一个非负的套接字描述符;失败时返回 -1,并设置
2. 绑定地址和端口 - bind
函数
cpp
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 参数说明:
sockfd
:由socket
函数返回的套接字描述符。addr
:指向sockaddr
结构体的指针,该结构体包含要绑定的地址和端口信息。对于 IPv4,通常使用sockaddr_in
结构体,并进行强制类型转换。addrlen
:addr
结构体的长度。
- 返回值:
- 成功时返回 0;失败时返回 -1,并设置
errno
。
- 成功时返回 0;失败时返回 -1,并设置
3. 发送数据 - sendto
函数
cpp
#include <sys/socket.h>
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
:指向目标地址的sockaddr
结构体指针,包含目标主机的地址和端口信息。addrlen
:dest_addr
结构体的长度。
- 返回值:
- 成功时返回实际发送的字节数;失败时返回 -1,并设置
errno
。
- 成功时返回实际发送的字节数;失败时返回 -1,并设置
4. 接收数据 - recvfrom
函数
cpp
#include <sys/socket.h>
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
:指向源地址的sockaddr
结构体指针,用于存储发送方的地址和端口信息。addrlen
:指向src_addr
结构体长度的指针,调用前设置为该结构体的大小,调用后会被更新为实际接收到的地址信息的长度。
- 返回值:
- 成功时返回实际接收的字节数;失败时返回 -1,并设置
errno
。
- 成功时返回实际接收的字节数;失败时返回 -1,并设置
5. 关闭套接字 - close
函数
cpp
#include <unistd.h>
int close(int fd);
- 参数说明:
fd
:要关闭的套接字描述符。
- 返回值:
- 成功时返回 0;失败时返回 -1,并设置
errno
。
- 成功时返回 0;失败时返回 -1,并设置
二、代码示例
1.服务端
cpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;
Log lg;
enum{
SOCKET_ERR=1,
BIND_ERR
};
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
class UdpServer{
public:
UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false)
{}
void Init()
{
// 1. 创建udp socket
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET
if(sockfd_ < 0)
{
lg(Fatal, "socket create error, sockfd: %d", sockfd_);
exit(SOCKET_ERR);
}
lg(Info, "socket create success, sockfd: %d", sockfd_);
// 2. bind socket
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??
// local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
}
void Run(func_t func) // 对代码进行分层
{
isrunning_ = true;
char inbuffer[size];
while(isrunning_)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
if(n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
inbuffer[n] = 0;
std::string info = inbuffer;
std::string echo_string = func(info);
sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len);
}
}
~UdpServer()
{
if(sockfd_>0) close(sockfd_);
}
private:
int sockfd_; // 网路文件描述符
std::string ip_; // 任意地址bind 0
uint16_t port_; // 表明服务器进程的端口号
bool isrunning_;
};
Init
方法用于初始化 UDP 服务器:- 调用
socket
函数创建一个 UDP 套接字,使用AF_INET
表示 IPv4 协议族,SOCK_DGRAM
表示 UDP 套接字。 - 如果套接字创建失败,使用日志记录错误信息并退出程序。
- 创建一个
sockaddr_in
结构体local
,用于存储服务器的地址信息。 - 使用
bzero
函数将local
结构体清零。 - 设置
local
结构体的sin_family
为AF_INET
,sin_port
为传入的端口号,并使用htons
函数将其转换为网络字节序。 - 使用
inet_addr
函数将传入的 IP 地址字符串转换为网络字节序的 32 位整数,并赋值给sin_addr.s_addr
。 - 调用
bind
函数将套接字绑定到指定的地址和端口。 - 如果绑定失败,使用日志记录错误信息并退出程序。
- 调用
Run
方法用于启动 UDP 服务器并处理客户端请求:- 将
isrunning_
标志设置为true
,表示服务器正在运行。 - 创建一个大小为
size
的字符数组inbuffer
,用于存储接收到的数据。 - 进入一个无限循环,不断接收客户端发送的数据。
- 调用
recvfrom
函数接收客户端发送的数据,并将客户端的地址信息存储在client
结构体中。 - 如果接收失败,使用日志记录错误信息并继续下一次循环。
- 在接收到的数据末尾添加字符串结束符
'\0'
,将其转换为std::string
类型的info
。 - 调用传入的函数对象
func
对接收到的数据进行处理,并将处理结果存储在echo_string
中。 - 调用
sendto
函数将处理结果发送回客户端。
- 将
2.客户端
cpp
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport); //?
server.sin_addr.s_addr = inet_addr(serverip.c_str());
socklen_t len = sizeof(server);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
cout << "socker error" << endl;
return 1;
}
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 系统什么时候给我bind呢?首次发送数据的时候
string message;
char buffer[1024];
while (true)
{
cout << "Please Enter@ ";
getline(cin, message);
// std::cout << message << std::endl;
// 1. 数据 2. 给谁发
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
if(s > 0)
{
buffer[s] = 0;
cout << buffer << endl;
}
}
close(sockfd);
return 0;
}
argc
表示命令行参数的数量,argv
是一个字符串数组,存储了命令行参数。- 如果命令行参数数量不等于 3(程序名、服务器 IP 地址、服务器端口号),则调用
Usage
函数打印使用说明并退出程序。 - 从命令行参数中获取服务器的 IP 地址和端口号。
std::stoi
函数将字符串类型的端口号转换为uint16_t
类型。- 创建一个
sockaddr_in
结构体server
,用于存储服务器的地址信息。 bzero
函数将server
结构体的内存清零。server.sin_family
设置为AF_INET
,表示使用 IPv4 地址族。server.sin_port
使用htons
函数将端口号从主机字节序转换为网络字节序。server.sin_addr.s_addr
使用inet_addr
函数将字符串类型的 IP 地址转换为网络字节序的 32 位整数。len
存储server
结构体的大小。socket
函数创建一个 UDP 套接字,使用AF_INET
表示 IPv4 地址族,SOCK_DGRAM
表示 UDP 协议。- 如果套接字创建失败,输出错误信息并返回 1。
- 创建一个
std::string
类型的message
用于存储用户输入的消息,创建一个字符数组buffer
用于存储从服务器接收的数据。 - 使用
getline
函数从标准输入读取用户输入的消息。 sendto
函数将消息发送给服务器,指定套接字描述符、消息内容、消息长度、标志位、服务器地址结构体和地址结构体的大小。- 创建一个
sockaddr_in
结构体temp
用于存储服务器的地址信息,len
存储temp
结构体的大小。 recvfrom
函数从服务器接收数据,指定套接字描述符、接收缓冲区、缓冲区大小、标志位、服务器地址结构体指针和地址结构体大小指针。- 如果接收到的数据长度大于 0,在缓冲区末尾添加字符串结束符
'\0'
,并输出接收到的数据。
总结
该程序实现了一个简单的 UDP 客户端,通过命令行参数指定服务器的 IP 地址和端口号,从用户输入获取消息并发送给服务器,同时接收服务器的响应并输出。客户端不需要显式绑定端口,操作系统会在首次发送数据时自动分配一个可用的端口。