目录
套接字地址结构(sockaddr)
1.Socket API
2.sockaddr结构
3. sockaddr、sockaddr_in 和 sockaddr_un 的关系
sockaddr 结构体
sockaddr_in 结构体(IPv4 套接字地址)
sockaddr_un 结构体(Unix域套接字地址)
4. sockaddr 通用结构的意义
5. 通用性带来的优势
6. IPv4 与 IPv6 的地址表示
7. 代码示例
in_addr 结构体(用于表示IPv4地址)
Socket 接口
1. 创建 Socket 文件描述符
2. 绑定 bind 端口号 (服务器)
3. 开始监听 Socket (TCP 服务器)
4. 接收连接请求 (TCP 服务器)
5. 建立连接 (TCP 客户端)
6. 设置套接字选项 (进阶)
7. 地址转换函数
8. 数据传输函数
sendto() (UDP)
recvfrom() (UDP)
send() 和 recv() (TCP)
9. 实际使用中的注意事项
INADDR_ANY
客户端是否需要绑定?
Listening Socket vs Connected Socket
10. TCP 通信流程
11. TCP vs UDP
12. popen() 和 pclose()
预备知识
简易的实验
Log.hpp
1. 头文件的引用
2. 宏定义
3. Log 类
成员变量
构造函数
Enable 方法
levelToString 方法
printLog 方法
printOneFile 方法
printClassFile 方法
重载的 operator()
4. 总结
Makefile
UdpServer.hpp
1. 头文件的引用
2. typedef 定义
3. 日志对象和错误码枚举
4. 全局变量
5. UdpServer 类
构造函数
Init 方法
Run 方法
析构函数
6. 总结
Main.cc
1. 头文件引用
2. Usage 函数
3. Handler 函数
4. ExcuteCommand 函数
5. main 函数
6. 程序的工作流程
7. 总结
UdpClient.cc
1. 头文件引用
2. Usage 函数
3. 主程序 main
参数检查与处理
服务器地址设置
创建UDP套接字
客户端是否需要 bind?
消息发送和接收
关闭套接字
4. 程序工作流程
5. 总结
注意
上篇文章我们所说 ip+port ----->该主机上对应的服务进程,是全网中是唯一的一个进程!
ip+port就是套接字,socket
套接字地址结构(sockaddr)
1.Socket API
Socket API
是一层网络编程接口,抽象了底层的网络协议,定义在 netinet/in.h
中。它适用于多种网络通信方式,如 IPv4、IPv6,以及 UNIX 域套接字(用于本地进程间通信)。通过 Socket API
,程序可以实现跨网络的进程间通信(如通过IP地址和端口号进行的网络通信),也可以实现本地的进程间通信。
常见 API(感知):
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
2.sockaddr结构
我们可以看到上面struct sockaddr *addr出现次数挺多的。实际上在网络上通信的时候套接字种类是比较多的,下面是常见的三种:
设计者想将网络接口统一抽象化--参数的类型必须是统一的,底层是一种多态的设计
运用场景:
- 网络套接字:运用于网络跨主机之间通信+本地通信
- unix域间套接字: 本地通信
- 我们现在在使用网络编程通信时是应用层调传输层的接口,而原始套接字:跳过传输层访问其他层协议中的有效数据。主要用于抓包,侦测网络情况。。
我们现在知道套接字种类很多,它们应用的场景也是不一样的。所以未来要完成这三种通信就需要有三套不同接口,但是思想上用的都是套接字的思想。因此接口设计者不想设计三套接口,只想设计一套接口,可以通过不通的参数,解决所有网络或者其他场景下的通信网络。
由于不同的通信方式(跨网络或本地通信)有不同的地址格式,套接字使用不同的结构体来封装地址信息:
sockaddr_in
:用于跨网络通信(例如通过 IP 和端口号进行通信)。sockaddr_un
:用于本地通信(通过文件路径进行通信)。
为了解决这些不同地址格式的兼容性问题,套接字提供了一个通用的地址结构体 sockaddr
,用于统一处理不同的地址结构。
3. sockaddr
、sockaddr_in
和 sockaddr_un
的关系
sockaddr
是一个通用的套接字地址结构,它为所有不同类型的通信方式提供了统一的接口。具体通信时,sockaddr
实际上指向特定的地址结构(如 sockaddr_in
或 sockaddr_un
),然后通过强制类型转换来区分是哪种通信方式。
这种设计类似于面向对象编程中的“多态”:sockaddr
可以看作一个“父类”,而 sockaddr_in
和 sockaddr_un
是它的“子类”。在程序中,套接字函数接受 sockaddr*
类型的参数,然后根据具体的通信类型进行处理。
sockaddr
结构体
struct sockaddr {__SOCKADDR_COMMON(sa_); /* 公共数据:地址家族和长度 */char sa_data[14]; /* 地址数据 */
};
sockaddr_in
结构体(IPv4 套接字地址)
struct sockaddr_in {__SOCKADDR_COMMON(sin_);in_port_t sin_port; /* 端口号 */struct in_addr sin_addr; /* IP地址 */unsigned char sin_zero[sizeof(struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof(in_port_t) - sizeof(struct in_addr)];
};
sockaddr_un
结构体(Unix域套接字地址)
struct sockaddr_un {__SOCKADDR_COMMON(sun_);char sun_path[108]; /* 文件路径 */
};
4. sockaddr
通用结构的意义
sockaddr
作为通用结构,它的前16个比特用于存储协议家族(sa_family
字段)。这个字段用来表明使用的是哪种通信方式:
通过这种设计,Socket API
可以通过统一的函数接口,处理不同类型的地址格式。开发者只需要将具体的地址结构转换为 sockaddr
,并设置协议家族字段,套接字函数就能识别出应该进行哪种通信。
5. 通用性带来的优势
Socket API
的这种设计带来了极大的通用性,使得开发者在同一套代码中可以处理不同的协议类型。例如,函数 sendto()
或 recvfrom()
可以接受 sockaddr*
作为参数,无论是处理 IPv4、IPv6 还是 UNIX Domain Socket,代码都不需要做出太大改动。
int sendto(int sockfd, const void *msg, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
在这个函数中,dest_addr
是一个通用的 sockaddr*
,程序只需根据实际使用的通信方式(如 IPv4 或 IPv6)对其进行强制类型转换即可。
6. IPv4 与 IPv6 的地址表示
- IPv4 地址格式 使用
sockaddr_in
结构体,地址类型是AF_INET
,端口号和IP地址需要转换为网络字节序(大端序)。 - IPv6 地址格式 使用
sockaddr_in6
结构体,地址类型是AF_INET6
。
7. 代码示例
in_addr
结构体(用于表示IPv4地址)
typedef uint32_t in_addr_t;
struct in_addr {in_addr_t s_addr; // 32位IP地址
};
in_addr
是一个32位的整数,用来表示IPv4的IP地址。通信过程中,IP地址通常是通过字符串格式(如 "192.168.1.1")转换为 in_addr_t
类型的数值来表示。
总结
- 通过
sockaddr
结构体,Socket API
实现了网络通信和本地通信的统一接口 - 它的设计理念类似于“多态”,即通过一个通用的接口来处理多种类型的地址格式
Socket 接口
1. 创建 Socket 文件描述符
在 TCP 和 UDP 通信中,首先要创建一个 Socket 文件描述符,它本质上是一个网络文件。其函数原型为:
- 功能:打开一个网络通讯端口,返回一个文件描述符,如果失败,返回 -1。
- 参数:
-
domain
:协议域,如AF_INET
(IPv4)、AF_INET6
(IPv6)、AF_LOCAL
(Unix域套接字)。type
:套接字类型,如SOCK_STREAM
(字节流,TCP)、SOCK_DGRAM
(数据报,UDP)。protocol
:协议类别,通常设置为 0,自动推导出对应的协议,如 TCP/UDP。
2. 绑定 bind 端口号 (服务器)
在服务器端,必须绑定一个 IP 地址和端口号,以便客户端可以与服务器建立通信。bind()
函数用于将套接字与 IP 和端口号绑定:
- 功能:将指定的 IP 和端口号绑定到套接字,使之监听指定地址。
- 参数:
-
socket
:套接字文件描述符。address
:存储地址和端口号的结构体指针。address_len
:地址结构体的长度。
3. 开始监听 Socket (TCP 服务器)
在服务器中,调用 listen()
函数使套接字进入监听状态,准备接受连接请求:
- 功能:让服务器套接字进入监听状态,准备接收客户端连接。
- 参数:
-
socket
:监听套接字描述符。backlog
:全连接队列的最大长度,用于处理多个客户端连接请求。
4. 接收连接请求 (TCP 服务器)
服务器使用 accept()
从连接队列中提取下一个连接请求,并返回新的套接字用于与客户端通信:
- 功能:获取一个已完成的连接请求,并返回新的套接字用于客户端通信。
- 参数:
-
socket
:监听套接字。address
:存储客户端的地址信息。address_len
:地址结构的长度。
5. 建立连接 (TCP 客户端)
客户端通过 connect()
向服务器发起连接请求:
- 功能:TCP 客户端使用该函数建立与服务器的连接。
- 参数:
-
sockfd
:用于通信的套接字文件描述符。addr
:服务器的地址。addrlen
:地址长度。
6. 设置套接字选项 (进阶)
通过 setsockopt()
可以设置套接字的各种属性,例如端口重用等高级功能:
- 功能:设置套接字的选项,如端口重用等。
- 参数:
-
sockfd
:套接字文件描述符。level
:选项的层次(如SOL_SOCKET
,IPPROTO_TCP
等)。optname
:选项名。optval
:指向设置值的指针。optlen
:设置值的长度。
7. 地址转换函数
IP 地址可以以字符串或整数形式存在。常见的地址转换函数包括:
- 字符串 IP 转整数 IP:
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr, void *addrptr);
- 整数 IP 转字符串 IP:
char *inet_ntoa(struct in_addr addr);
const char *inet_ntop(int family, const void *src, char *dest, size_t len);
8. 数据传输函数
sendto()
(UDP)
用于在 UDP 协议下发送数据:
- 功能:发送数据到指定地址。
- 参数:
-
sockfd
:套接字文件描述符。buf
:要发送的数据。len
:数据长度。
recvfrom()
(UDP)
接收来自远程主机的数据:
- 功能:接收数据。
- 参数:
-
sockfd
:套接字文件描述符。buf
:存放接收数据的缓冲区。
send()
和 recv()
(TCP)
send()
用于在 TCP 协议下发送数据。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv()
用于接收 TCP 协议下的数据。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
9. 实际使用中的注意事项
INADDR_ANY
在服务器端,INADDR_ANY
(0.0.0.0)可以让服务器监听所有可用的网络接口,而不必指定具体的 IP 地址。这种方式提高了代码的可移植性。
local.sin_addr.s_addr = INADDR_ANY;
客户端是否需要绑定?
客户端不需要手动绑定端口,操作系统会自动选择一个可用的端口。除非明确需要使用固定的端口,否则不建议手动绑定。
Listening Socket vs Connected Socket
- Listening Socket:服务器使用它来监听连接请求。它在整个服务器生命周期内存在。
- Connected Socket:服务器接收连接请求后,用于与客户端通信的套接字。每个客户端有一个独立的连接套接字。
10. TCP 通信流程
- 服务器初始化:调用
socket()
创建套接字,调用bind()
绑定地址和端口,调用listen()
进入监听状态。 - 客户端连接:客户端通过
socket()
创建套接字,使用connect()
发起连接请求。 - 三次握手:TCP 客户端与服务器通过三次握手建立连接。
- 数据传输:连接建立后,双方可以通过
send()
和recv()
进行数据传输。 - 断开连接:通过四次挥手,客户端和服务器断开连接。
这里只是简单提一下,要有个印象,下篇文章将详细讲解~
11. TCP vs UDP
- TCP:可靠的连接,字节流传输,保证数据顺序。
- UDP:不可靠传输,数据报传输,适用于实时通信。
12. popen()
和 pclose()
popen()
创建一个管道用于与子进程通信:来实现命令行通信
FILE *popen(const char *command, const char *type);
pclose()
关闭通过 popen()
打开的文件指针:
int pclose(FILE *stream);
使用场景:Udpcommand
结语
Socket 编程是网络编程的基础,通过 Socket API,开发者可以实现 TCP 和 UDP 通信。了解每个函数的作用、参数和使用场景,可以帮助开发者在构建高效、稳定的网络应用时得心应手。
预备知识
这个部分的设计非常重要
struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_); //主机转网络,16位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);
端口号,是要在网络部分,来回传递的
一个关于绑定 IP 的问题
转化代码 local.sin_addr.s_addr = inet_addr(ip_.c_str());
一个关于 port 的问题
转化代码 local.sin_addr.s_addr = htonl(INADDR_ANY);
关于 port 的测试报错,要 sudo 用超级用户去绑
[0,1023]: 系统内定的端口号,一般都要有固定的应用层协议使用,http:80,https:443,mysql:3306...
简易的实验
Log.hpp
#pragma once
#include <iostream>
#include<time.h>
#include<stdarg.h>//这个头文件是干啥的
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>//?
#include<unistd.h>
#include<stdlib.h>#define SIZE 1024#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"
class Log
{public:Log(){printfMethod=Screen;path="./log/";}void Enable(int method){printfMethod=method;}//将日志级别转化为对应的字符串表示形式std::string levelToString(int level)
{switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}
}
//根据日志的输出方式,将日志输出到相应的目标
void printLog(int level, const std::string &logtxt)
{switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}
}
//将日志输出到指定文件,文件名通过将路径和文件名拼接得到,并以追加的方式写入日志
void printOneFile(const std::string &logname,const std::string &logtxt)
{std::string _logname=path+logname;int fd=open(_logname.c_str(),O_WRONLY | O_CREAT | O_APPEND,0666);if(fd<0)return;write(fd, logtxt.c_str(), logtxt.size());close(fd);
}
//根据日志级别输出到不同的文件
void printClassFile(int level, const std::string &logtxt)
{std::string filename = LogFile;filename += ".";filename += levelToString(level);printOneFile(filename, logtxt);
}
//重载的operator()使得Log类的实例可以像函数一样调用,简化日志记录的使用
//使用可变参数处理日志消息的格式化
void operator()(int level, const char *format, ...)
{time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);printLog(level, logtxt);
}
private:int printfMethod;std::string path;
};
这段代码实现了一个简单的日志系统,允许将日志输出到控制台、单一日志文件或根据日志级别分类的不同文件中。下面是对代码的详细解释和整理。分析:
1. 头文件的引用
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
这些头文件提供了实现该日志系统所需的基本库功能:
<iostream>
用于控制台输入输出。<time.h>
用于处理时间。<stdarg.h>
用于处理可变参数列表。<sys/types.h>
,<sys/stat.h>
,<fcntl.h>
,<unistd.h>
提供文件操作相关的系统调用。<stdlib.h>
提供了一些标准库函数,如exit()
等。
2. 宏定义
#define SIZE 1024#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"
这些宏定义了一些常量:
SIZE
定义了缓冲区的大小。Info
,Debug
,Warning
,Error
,Fatal
定义了日志级别。Screen
,Onefile
,Classfile
定义了日志的输出方式。LogFile
定义了默认的日志文件名。
3. Log
类
Log
类是日志系统的核心,包含了日志输出的主要功能。
成员变量
private:int printMethod;std::string path;
printMethod
用于保存日志的输出方式(屏幕、单一文件、分类文件)。path
保存日志文件的路径。
构造函数
public:Log(){printMethod = Screen;path = "./log/";}
- 构造函数初始化日志输出方式为屏幕输出 (
Screen
),并设置日志文件的默认路径为"./log/"
。
Enable
方法
void Enable(int method)
{printMethod = method;
}
- 这个方法用于设置日志的输出方式。
levelToString
方法
std::string levelToString(int level)
{switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}
}
- 将日志级别转换为对应的字符串表示形式。
printLog
方法
void printLog(int level, const std::string &logtxt)
{switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}
}
- 根据日志输出方式,将日志输出到相应的目标(屏幕、单一文件或分类文件)。
printOneFile
方法
void printOneFile(const std::string &logname, const std::string &logtxt)
{std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0)return;write(fd, logtxt.c_str(), logtxt.size());close(fd);
}
- 将日志输出到指定文件。文件名通过将路径和文件名拼接得到,并以追加方式写入日志。
printClassFile
方法
void printClassFile(int level, const std::string &logtxt)
{std::string filename = LogFile;filename += ".";filename += levelToString(level);printOneFile(filename, logtxt);
}
- 根据日志级别输出到不同的文件,例如
log.txt.Debug
或log.txt.Error
。
重载的 operator()
void operator()(int level, const char *format, ...)
{time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);printLog(level, logtxt);
}
- 重载的
operator()
使得Log
类的实例可以像函数一样调用,简化了日志记录的使用。 - 使用可变参数处理日志消息的格式化,并根据当前时间戳和日志级别生成日志消息。
4. 总结
实现了一个简单而灵活的日志系统,可以根据用户需求将日志输出到不同的目标。日志级别和输出方式都可以通过方法进行配置,使用方便。预留了部分扩展功能,例如注释掉的 logmessage
方法,可以进行进一步扩展和定制。
Makefile
.PHONY:all
all:udpserver udpclientudpserver:Main.ccg++ -o $@ $^ -std=c++11
udpclient:UdpClient.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f udpserver udpclient
UdpServer.hpp
#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 socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INETif(sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);// 2. bind socketstruct 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 0uint16_t port_; // 表明服务器进程的端口号bool isrunning_;
};
这段代码实现了一个简单的UDP服务器,提供了初始化和运行的功能,并通过一个回调函数处理接收到的数据。以下是对代码的详细解释和整理。
1. 头文件的引用
#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"
<iostream>
:用于标准输入输出流。<string>
:提供了std::string
类。<strings.h>
:提供了bzero
函数用于清空内存。<cstring>
:用于C字符串操作,比如strerror
函数。<sys/types.h>
:定义了系统数据类型,如socklen_t
。<sys/socket.h>
:提供了套接字接口。<netinet/in.h>
:定义了 Internet 地址族所需的数据结构和宏,如struct sockaddr_in
和htons
等。<arpa/inet.h>
:提供了用于处理IP地址的函数,如inet_addr
。"Log.hpp"
:自定义的日志系统,方便记录日志信息。
2. typedef
定义
typedef std::function<std::string(const std::string&)> func_t;
func_t
定义了一个类型,该类型表示一个接受std::string
参数并返回std::string
的函数对象。这种类型用于处理接收到的数据。
3. 日志对象和错误码枚举
Log lg;enum{SOCKET_ERR = 1,BIND_ERR
};
Log lg;
:创建一个全局的日志对象lg
,用于记录日志信息。enum
定义了一些错误码,便于在出现错误时通过exit
函数终止程序。
4. 全局变量
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
defaultport
和defaultip
定义了默认的端口号和IP地址。size
定义了接收缓冲区的大小。
5. UdpServer
类
UdpServer
类实现了UDP服务器的核心功能,包括初始化、运行和析构。
构造函数
UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip): sockfd_(0), port_(port), ip_(ip), isrunning_(false) {}
- 构造函数初始化服务器的端口、IP、文件描述符等变量,默认使用全局定义的端口和IP。
Init
方法
void Init()
{sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}lg(Info, "socket create success, sockfd: %d", sockfd_);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());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));
}
socket
函数创建一个UDP套接字,如果失败,则记录日志并退出。bzero
函数将local
结构体清零。htons
和inet_addr
函数用于处理端口号和IP地址的转换。bind
函数将套接字绑定到指定的IP地址和端口号。如果绑定失败,则记录日志并退出。
Run
方法
对代码进行分层
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);}
}
Run
方法是服务器的核心运行逻辑。它接受一个func_t
类型的函数,用于处理接收到的数据。- 在循环中,服务器使用
recvfrom
接收来自客户端的数据,处理后再使用sendto
发送响应。 - 如果
recvfrom
失败,记录警告日志并继续下一次接收。
析构函数
~UdpServer()
{if(sockfd_ > 0) close(sockfd_);
}
- 析构函数负责关闭套接字,释放资源。
6. 总结
实现一个UDP服务器的基本框架,通过日志系统记录服务器的运行状态,并允许用户通过回调函数自定义数据处理逻辑。服务器可以接收客户端的数据,并根据用户定义的处理逻辑返回相应的数据。
Main.cc
使用 UdpServer
类构建一个简单的UDP服务器,并且通过一个回调函数处理接收到的命令。服务器的核心功能是接收客户端发送的消息,然后执行特定的命令,并将结果返回给客户端。
1. 头文件引用
#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
"UdpServer.hpp"
:包含之前定义的UdpServer
类,用于创建和管理UDP服务器。<memory>
:提供智能指针std::unique_ptr
,用于管理动态分配的对象。<cstdio>
:提供C语言的标准输入输出函数,如popen
、fgets
和pclose
。
2. Usage
函数
void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
Usage
函数用于在命令行参数不正确时,向用户显示正确的使用方法。它会输出如何正确地执行程序及端口号的要求(1024以上)。
3. Handler
函数
std::string Handler(const std::string &str)
{std::string res = "Server get a message: ";res += str;std::cout << res << std::endl;// pid_t id = fork();// if(id == 0)// {// // ls -a -l -> "ls" "-a" "-l"// // exec*();// }return res;
}
- 该函数用于处理从客户端接收到的消息,简单地将接收到的消息加上前缀
"Server get a message: "
后返回,并在服务器端输出消息内容。 - 函数中注释掉的
fork()
和exec()
代码提示可能有计划在子进程中执行命令,但目前这部分未实现。
4. ExcuteCommand
函数
std::string ExcuteCommand(const std::string &cmd)
{FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return "error";}std::string result;char buffer[4096];while(true){char *ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr) break;result += buffer;}pclose(fp);return result;
}
- 该函数使用
popen()
执行来自客户端的命令,并将命令的输出结果返回给客户端。其工作流程是:
-
- 通过
popen
打开子进程并执行传入的命令。 - 使用
fgets
逐行读取子进程的输出,并将其存储到result
字符串中。 - 读取完成后通过
pclose
关闭文件指针,返回命令的执行结果。
- 通过
5. main
函数
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init(/**/);svr->Run(ExcuteCommand);return 0;
}
main
函数是程序的入口,负责启动UDP服务器并处理命令行参数。
-
- 首先检查命令行参数是否正确(即端口号是否提供),如果不正确则调用
Usage
函数输出使用方法并退出。 - 使用
std::stoi
将命令行参数(端口号)转换为uint16_t
类型。 - 通过
std::unique_ptr
创建一个UdpServer
实例,使用智能指针管理UdpServer
对象的生命周期,确保资源在退出时被正确释放。 - 调用
UdpServer
的Init()
方法初始化服务器。 - 调用
Run()
方法开始服务器运行,并将ExcuteCommand
函数作为回调函数传递给UdpServer
,服务器接收到客户端消息后将会调用该函数来执行命令。
- 首先检查命令行参数是否正确(即端口号是否提供),如果不正确则调用
6. 程序的工作流程
- 服务器启动时,用户通过命令行提供一个端口号。
- 服务器初始化并绑定到指定端口号。
- 服务器进入循环,等待客户端发送消息。
- 一旦服务器接收到消息,它会调用
ExcuteCommand
函数执行接收到的命令,并将结果返回给客户端。
7. 总结
通过 UdpServer
类实现网络通信,接收到客户端的消息后,通过 ExcuteCommand
函数执行来自客户端的命令。服务器使用智能指针管理 UdpServer
的生命周期,确保资源管理的安全性。
UdpClient.cc
客户端需要绑定吗?一定需要
这段代码实现了一个简单的UDP客户端,它可以向指定的服务器发送消息并接收服务器的响应。客户端通过命令行参数指定服务器的IP地址和端口号,使用UDP协议进行通信。
1. 头文件引用
#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>
2. Usage
函数
void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}
- 该函数用于提示用户如何正确使用命令行参数。需要输入程序名、服务器IP地址和端口号。
3. 主程序 main
参数检查与处理
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]);
- 主程序首先检查命令行参数的数量,确保用户输入了正确数量的参数(服务器IP和端口号)。如果参数不对,调用
Usage()
输出提示并退出。 std::stoi(argv[2])
将输入的端口号从字符串转换为uint16_t
类型。
服务器地址设置
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);
- 通过
sockaddr_in
结构体设置服务器的地址信息。
-
bzero
将server
结构体清空。sin_family
设置为AF_INET
表示使用IPv4地址族。sin_port
使用htons
将端口号转换为网络字节序(大端序),确保与服务器端匹配。sin_addr.s_addr
使用inet_addr()
将服务器IP地址字符串转换为合适的格式。
创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socket error" << endl;return 1;}
socket()
创建一个UDP套接字。如果创建失败(返回值小于0),程序输出错误信息并退出。
-
AF_INET
指定使用IPv4。SOCK_DGRAM
指定UDP协议。
客户端是否需要 bind
?
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!// 系统什么时候给我bind呢?首次发送数据的时候
- 注释解释了客户端的
bind
行为。客户端不需要显式调用bind
,操作系统会在首次发送数据时自动选择一个可用的端口号。
消息发送和接收
string message;char buffer[1024];while (true){cout << "Please Enter@ ";getline(cin, message);// 发送数据到服务器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;}}
- 通过循环持续等待用户输入消息,使用
getline()
获取用户输入的字符串。 sendto()
函数将输入的消息发送到服务器,参数包括:
-
sockfd
:套接字描述符。message.c_str()
:要发送的数据。message.size()
:数据长度。(struct sockaddr *)&server
:服务器的地址信息。
recvfrom()
函数从服务器接收响应:
-
buffer
用于存储接收到的数据,最大长度为1023。- 数据接收到后,将结尾补上空字符(
buffer[s] = 0
)并输出。
关闭套接字
close(sockfd);return 0;
}
- 当程序退出时,使用
close()
关闭套接字,释放资源。
4. 程序工作流程
- 启动客户端时,用户需要提供服务器的IP地址和端口号。
- 创建UDP套接字,并将服务器的地址信息(IP和端口)封装在
sockaddr_in
结构体中。 - 客户端进入一个循环,等待用户输入消息。
- 用户输入消息后,客户端使用
sendto()
将消息发送到服务器。 - 客户端通过
recvfrom()
等待服务器的响应,并将接收到的数据打印到控制台。 - 程序关闭套接字并退出。
5. 总结
该程序实现了一个UDP客户端,可以向指定的服务器发送消息并接收响应。通过简单的 sendto()
和 recvfrom()
函数,客户端能够与UDP服务器进行通信。
注意
我们要打开我们的云服务器的特定的端口--开放端口的行为
下篇文章将继续进行优化,例如添加 safecheck command,实现一个聊天室,window 做客户端,linux 做服务器~下篇文章,敬请期待~