目录
[1.Socket API](#1.Socket API)
[3. sockaddr、sockaddr_in 和 sockaddr_un 的关系](#3. sockaddr、sockaddr_in 和 sockaddr_un 的关系)
[sockaddr 结构体](#sockaddr 结构体)
[sockaddr_in 结构体(IPv4 套接字地址)](#sockaddr_in 结构体(IPv4 套接字地址))
[sockaddr_un 结构体(Unix域套接字地址)](#sockaddr_un 结构体(Unix域套接字地址))
[4. sockaddr 通用结构的意义](#4. sockaddr 通用结构的意义)
[5. 通用性带来的优势](#5. 通用性带来的优势)
[6. IPv4 与 IPv6 的地址表示](#6. IPv4 与 IPv6 的地址表示)
[7. 代码示例](#7. 代码示例)
[in_addr 结构体(用于表示IPv4地址)](#in_addr 结构体(用于表示IPv4地址))
[Socket 接口](#Socket 接口)
[1. 创建 Socket 文件描述符](#1. 创建 Socket 文件描述符)
[2. 绑定 bind 端口号 (服务器)](#2. 绑定 bind 端口号 (服务器))
[3. 开始监听 Socket (TCP 服务器)](#3. 开始监听 Socket (TCP 服务器))
[4. 接收连接请求 (TCP 服务器)](#4. 接收连接请求 (TCP 服务器))
[5. 建立连接 (TCP 客户端)](#5. 建立连接 (TCP 客户端))
[6. 设置套接字选项 (进阶)](#6. 设置套接字选项 (进阶))
[7. 地址转换函数](#7. 地址转换函数)
[8. 数据传输函数](#8. 数据传输函数)
[send() 和 recv() (TCP)](#send() 和 recv() (TCP))
[9. 实际使用中的注意事项](#9. 实际使用中的注意事项)
[Listening Socket vs Connected Socket](#Listening Socket vs Connected Socket)
[10. TCP 通信流程](#10. TCP 通信流程)
[11. TCP vs UDP](#11. TCP vs UDP)
[12. popen() 和 pclose()](#12. popen() 和 pclose())
[1. 头文件的引用](#1. 头文件的引用)
[2. 宏定义](#2. 宏定义)
[3. Log 类](#3. Log 类)
[Enable 方法](#Enable 方法)
[levelToString 方法](#levelToString 方法)
[printLog 方法](#printLog 方法)
[printOneFile 方法](#printOneFile 方法)
[printClassFile 方法](#printClassFile 方法)
[重载的 operator()](#重载的 operator())
[4. 总结](#4. 总结)
[1. 头文件的引用](#1. 头文件的引用)
[2. typedef 定义](#2. typedef 定义)
[3. 日志对象和错误码枚举](#3. 日志对象和错误码枚举)
[4. 全局变量](#4. 全局变量)
[5. UdpServer 类](#5. UdpServer 类)
[Init 方法](#Init 方法)
[Run 方法](#Run 方法)
[6. 总结](#6. 总结)
[1. 头文件引用](#1. 头文件引用)
[2. Usage 函数](#2. Usage 函数)
[3. Handler 函数](#3. Handler 函数)
[4. ExcuteCommand 函数](#4. ExcuteCommand 函数)
[5. main 函数](#5. main 函数)
[6. 程序的工作流程](#6. 程序的工作流程)
[7. 总结](#7. 总结)
[1. 头文件引用](#1. 头文件引用)
[2. Usage 函数](#2. Usage 函数)
[3. 主程序 main](#3. 主程序 main)
[客户端是否需要 bind?](#客户端是否需要 bind?)
[4. 程序工作流程](#4. 程序工作流程)
[5. 总结](#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 域间套接字编程--同一个机器内
- 原始套接字编程--网络工具
- 网络套接字编程--用户间的网络通信
设计者想将网络接口统一抽象化--参数的类型必须是统一的,底层是一种多态的设计
运用场景:
- 网络套接字:运用于网络跨主机之间通信+本地通信
- unix域间套接字: 本地通信
- 我们现在在使用网络编程通信时是应用层调传输层的接口,而原始套接字:跳过传输层访问其他层协议中的有效数据。主要用于抓包,侦测网络情况。。
我们现在知道++套接字种类很多,它们应用的场景也是不一样的++。所以未来要完成这三种通信就需要有三套不同接口,但是思想上用的都是套接字的思想。因此接口设计者不想设计三套接口,只想设计一套接口,可以通过不通的参数,解决所有网络或者其他场景下的通信网络。
由于不同的通信方式(跨网络或本地通信)有不同的地址格式,套接字使用不同的结构体来封装地址信息:
s
ockaddr_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
字段)。这个字段用来表明使用的是哪种通信方式:
AF_INET
:IPv4网络通信。AF_INET6
:IPv6网络通信。AF_UNIX
:本地通信(UNIX 域套接字)。
通过这种设计,++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 udpclient
udpserver:Main.cc
g++ -o $@ $^ -std=c++11
udpclient:UdpClient.cc
g++ -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 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_;
};
这段代码实现了一个简单的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 做服务器~下篇文章,敬请期待~