第二部分(上):套接字
目录
1、socket编程
2、UTP
2.1、UTP接口
2.2、UTP应用
2.2.1、远程控制
2.2.2、聊天室
1、socket编程
基于IP地址和端口号的通信方式,就叫做socket(套接字)。
套接字编程的种类:
1、域间套接字编程。机器内进行通信,也就是本地通信,这个比较简单。
2、原始套接字编程。用网络层甚至是数据链路层的接口,一般用于一些网络工具。
3、网络套接字编程。用传输层接口,用户间的网络通信。
虽然套接字的有不同的种类,但是使用的都是同一套接口。
2、UTP
2.1、UTP接口
例如:使用socket函数创建一个套接字。
#include <sys/socket.h>int socket(int domain, int type, int protocol);
其中,domain这个参数指定套接字使用的地址类型和通信范围,决定了数据传输的网络层协议,可选的参数有很多,下面仅举几个:
1、AF_INET:IPv4 地址族,用于 IPv4 网络通信(最常用)。
2、AF_INET6:IPv6 地址族,用于 IPv6 网络通信。
3、AF_UNIX(或 AF_LOCAL):本地通信(进程间通信)。
type这个参数指定套接字的传输层通信方式,决定了数据传输的可靠性和特性。可选参数有:
1、SOCK_STREAM是指面向字节流的,基于TCP协议。
2、SOCK_DGRAM表示面向数据报的,基于UDP协议。
第三个参数protocol一般填0即可。
该函数成功返回一个网络文件描述符(非负整数),失败返回-1,并设置errno。
例如:使用bind函数将套接字与IP和端口进行绑定。
#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其中,sockfd传的就是用socket的返回值;addr传的是一个结构体,要传的结构体如下图所示

其中struct sockaddr_in是用来网络套接字编程的,struct sockaddr_un是用来域间套接字编程的。addr这个参数在使用的时候需要强制转化成其他的类型,这样的好处是程序的通用性,可以接收IPv4,IPv6,以及UNIX Domain Socket各种类型的结构体指针做为参数。
虽然bind的接口是struct sockaddr*类型的结构体,但是我们真正在基于IPv4编程时,使用的数据结构是struct sockaddr_in,这个结构里主要有三部分信息:地址类型、端口号、IP地址。该结构体中的字段如下图所示:


![]()
![]()

其中sin_family应该为AF_INET,sin_port是指端口号(网络字节序),sin_addr中的s_addr是指IP地址(网络字节序),sin_zero为填充字段,不需要管。
如果是IPv6则使用struct sockaddr_in6。
第三个参数addrlen传的是addr这个结构体的大小。
该函数成功返回0,失败返回-1,并且errno被设置。
注:IPv4、IPv6类型分别定义为常数AF_INET、AF_INET6,这样,只要取得某种结构体的首地址,不需要知道具体是哪种类型的结构体,就可以根据类型字段确定结构体中的内容。
注意:在云服务器上禁止绑定公网IP(但是在虚拟机上是可以的),因为云服务器上的公网IP地址并不是主机上的IP地址,他被虚拟化了;此外写服务一般也不喜欢绑定固定的IP地址,因为有可能一个主机有多个IP地址,为了使多个IP地址都可以被访问一般会选择绑定0.0.0.0这个IP地址,这样做的结果就是凡是发给这台主机的数据,都会根据端口号进行向上交付。
注:127.0.0.1这个IP地址可以在任意服务器上绑定,127.0.0.1是本地环回地址,通常用于客户端和服务器的测试,只用于本地进程间通信。
注意:端口号0到1023是系统内定的端口号,一般都是由固定的应用层协议来使用,例如http占用的是80端口号,https占用的是443端口号,如果我们去绑定这些端口的话会失败,除非使用root的权限(但是如果该端口已经被占用,使用root权限也不行);有一个比较特别的是mysql数据库,占用的端口为3306;一般我们绑定端口是尽量绑定大一点的。
例如:使用bzero函数将指定的内存空间初始化为0。
#include <strings.h>void bzero(void s[.n], size_t n);
其中,s表示地址,n表示初始化的空间大小。
例如:使用inet_addr函数将字符串的IPv4的地址转换成32位无符号整数形式的网络字节序列。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>in_addr_t inet_addr(const char *cp);
其中cp是字符串指针,指向字符串IP;成功返回值为返回转换后的32位网络字节序地址,失败返回-1。
例如:使用recvfrom函数来接收数据。
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len,int flags,struct sockaddr *_Nullable restrict src_addr,socklen_t *_Nullable restrict addrlen);
其中,sockfd是指socket函数的返回值;buf指一段获取数据的空间,len是指buf的大小;flags设置为0,表示使用阻塞方式,先设为0即可;src_addr是用来获取客户端的信息的,用于存储发送方的信息(IP 地址和端口号等等),若不需要,可设为NULL;addrlen指src_addr的大小,若src_addr为NULL,此参数也需设为NULL;函数成功返回收到了多少个字节,失败返回-1。
例如:使用sendto函数发送数据。
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void buf[.len], size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
其中,sockfd是指socket的返回值;buf指一段发送数据的空间,len是指buf的大小;flags默认设为0即可;dest_addr指定接收方的目标地址(IP地址和端口号等);addrlen是指dest_addr指向的地址结构的大小。成功返回实际发送的字节数,失败返回-1。
注意:服务器是要进行开放端口的,如果不进行开放端口的话,是没法访问云服务器的。
例如:下面介绍一个命令,该命令的作用是一个用于查询系统网络连接状态的命令。
netstat -naup
选项:
1、-n:以数字形式显示IP地址和端口。
2、-a:显示所有网络连接状态。
3、-u:仅显示UDP协议的连接。
4、-p:显示每个连接对应的进程 ID(PID)和进程名(需要root权限才能显示所有进程信息,普通用户可能只能看到自己的进程)。
上面的命令使用sudo权限的运行结果为:

其中,proto表示用的协议;Recv-Q和Send-Q分别表示收和发报文的个数;Local Address指本地地址,里面是IP地址+端口号;Foreign Address是指远端,里面为0.0.0.0:*表示能收到来自任何远端发送的消息;State指状态;PID/Program name指进程pid和名字。
例如:popen函数是用来执行外部命令的函数,pclose函数是用来关闭的。
#include <stdio.h>FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
其中popen函数会创建一个管道,fork一个子进程,在子进程中执行指定的command命令,并返回一个文件指针(FILE*),用于在父进程与子进程之间传递数据。type指定通信方向,只能是以下两种:
1、"r":父进程从子进程读取数据(子进程的标准输出通过管道传递给父进程)。
2、"w":父进程向子进程写入数据(父进程的输出通过管道传递给子进程的标准输入)。
成功返回一个与管道关联的FILE*指针,失败则返回NULL,并设置errno。
pclose函数关闭由popen()创建的文件指针,等待子进程结束,并返回子进程的退出状态,stream是由popen()返回的文件指针。成功返回子进程的退出状态,失败:返回 -1,并设置 errno。
例如:使用inet_ntoa函数将32位整数形式的网络字节序的IP地址转换为字符串。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>char *inet_ntoa(struct in_addr in);
inet_ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果,那么是否需要调用者手动释放呢?

man手册上说,inet_ntoa函数,是把这个返回结果放到了静态缓冲区(也就是使用了static),这个时候不需要我们手动进行释放。如果我们调用多次这个函数,会导致覆写。
另外inet_ntoa函数不是线程安全的函数,在多线程环境下,推荐使用inet_ntop函数,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。
例如:使用inet_ntop函数将二进制形式的IP地址(网络字节序)转换为字符串形式,同时支持IPv4和IPv6地址,并且解决了inet_ntoa的线程不安全的问题。
#include <arpa/inet.h>const char *inet_ntop(int af, const void *restrict src, char dst[restrict .size], socklen_t size);
其中,af指定要转换的IP类型,如果是AF_INET,表示转换 IPv4 地址;如果是AF_INET6,表示转换IPv6地址。src指向IP地址的指针,对于IPv4,src 应指向struct in_addr 类型(存储 32 位网络字节序地址);对于 IPv6,src 应指向 struct in6_addr 类型(存储128位网络字节序地址);dst是用户提供的缓冲区,用于存储转换后的字符串;size是指dst缓冲区的大小,需足够容纳转换后的字符串(包括终止符 \0)。
2.2、UTP应用
2.2.1、远程控制
简单的实现一台主机使用命令远程控制另一台主机。
Log.hpp
#pragma once#include <iostream>
#include <stdarg.h> // 使用可变参数列表需要用到这个头文件
#include <ctime>
#include <cstdlib>#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define SIZE 1024#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen; // 默认为屏幕打印path = "./log/";}void Enable(int method) // 更换日志的打印方式{printMethod = 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);}~Log(){}void operator()(int level, const char *format, ...){char leftbuffer[1024]; // 一条日志左边的格式信息,包括日志等级和时间。time_t t = time(nullptr); // 返回值是时间戳struct tm *ctime = localtime(&t); // 该函数可以将时间戳转换成一个struct tm 结构。// 下面的\是续行符,加不加都行。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);// printf("%s", logtxt);printLog(level, logtxt); // 打印}private:int printMethod; // 日志打印的方式std::string path; // 日志打印路径
};
Main.cc
#include "UdpServer.hpp"
#include "Log.hpp"#include <memory>
#include <cstdio>
#include <vector>void Usage(std::string proc) // 使用说明
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}bool SafeCheck(const std::string& cmd) // 简单的安全检查
{int safe = false;std::vector<std::string> key_word = {"rm", "sudo", "apt", "yum"};for(auto& word : key_word){auto pos = cmd.find(word);if(pos != std::string::npos) return false;}return true;
}std::string ExcuteCommand(const std::string& cmd) // 执行命令并获取结果
{std::cout << "Execute command: " << cmd << std::endl;if(!SafeCheck(cmd)) return "Bad man";FILE* fp = popen(cmd.c_str(), "r");if (nullptr == fp){perror("popen error");return "error";}char buffer[4096];std::string result;while (true){char* res = fgets(buffer, sizeof(buffer), fp);if (nullptr == res) break;result += buffer;}pclose(fp);return result;
}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;
}
UdpServer.hpp
#pragma once#include <iostream>
#include <string>
#include <strings.h>
#include <string.h>
#include <functional>#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Log.hpp"// using func_t = std::function<std::string(const std::string&)>; // 参数为string返回值为string的函数类型
typedef std::function<std::string(const std::string&)> func_t; // 和上面的写法等价Log lg;enum{SOCKET_ERR = 1, // socket创建失败BIND_ERR // 绑定失败
};std::string defaultip = "0.0.0.0"; // 任意地址绑定
uint16_t defaultport = 19000; // 默认端口号
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、创建socketsockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 注:AF_INET也可以写成PF_INET,两者值相同。if (sockfd_ < 0){lg(Fatal, "socket creation failed: %d, error string: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "socket created successfully: %d", sockfd_);// 2、绑定端口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()); // 将字符串转换成uint32_t类型,再将uint32_t转化为网络字节序IP地址。 //local.sin_addr.s_addr = INADDR_ANY; // 绑定任意地址,也可以这么写。if (bind(sockfd_, (const struct sockaddr*)&local, sizeof(local)) < 0){lg(Fatal, "socket bind failed: %d, error string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "socket binded successfully");}void Run(func_t func) // 运行服务器{isrunning_ = true;char inbuffer[size];while (isrunning_){// 3、接收数据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: %d, error string:%s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr);inbuffer[n] = 0;std::string info = inbuffer;std::string echo_string = func(info);// 4、发送数据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_; // IP地址uint16_t port_; // 端口号bool isrunning_; // 服务器运行状态
};
linux下的客户端UdpClient.cc
#include <iostream>
#include <cstdint>#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include <strings.h>using namespace std;void Usage(std::string proc) // 使用说明
{cout << "\n\rUsage: " << proc << " severip severport\n" << endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}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;}// 客户端也是要进行bind的,只不过不需要我们显示的bind,系统会自动的bind。// 在UDP这一块,是在首次发送数据时,进行绑定的。string massage;char buffer[1024];while (true){cout << "Please Enter@ ";getline(cin, massage);// 发送数据sendto(sockfd, massage.c_str(), massage.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;
}
Windows下的客户端UdpClient.cc
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstdio>#include <WinSock2.h>
#include <Windows.h>#pragma comment(lib, "ws2_32.lib") // 在链接阶段自动链接ws2_32.lib这个库文件
#pragma warning(disable:4996) // 禁用编译器的4996号警告uint16_t serverport = 19000;
std::string serverip = "43.138.124.222";int main()
{/** WinSock 库并非 Windows 系统默认加载的,需要通过WSAStartup显式初始化,告诉系统 “我要使用网络功能”,* 并确认系统支持所需的版本。如果不执行下面这两行代码,* 后续的socket、sendto等网络函数会直接失败(返回错误值),程序无法进行网络通信。*/WSADATA wsd;WSAStartup(MAKEWORD(2, 2), &wsd); struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == SOCKET_ERROR) // SOCKET_ERROR的值就是-1{std::cerr << "socker error" << std::endl;return 1;}std::string massage;char buffer[1024];while (true){std::cout << "Please Enter@ ";std::getline(std::cin, massage);// 发送数据sendto(sockfd, massage.c_str(), (int)massage.size(), 0, (struct sockaddr*)&server, sizeof(server));struct sockaddr_in temp;int len = sizeof(temp);// 接收数据int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}#ifdef _WIN32// Windows平台使用closesocket,关闭之前创建的套接字并释放其占用的资源,不能使用close来关闭。closesocket(sockfd);
#else// Linux/Unix平台使用closeclose(sockfd);
#endifWSACleanup(); // 终止WinSock库的使用并释放其占用的系统资源。return 0;
}
注:不管是什么操作系统,网络一定是一样的,提供的接口也是一样的,,区别是像Windows这种操作系统在应用层对个别的数据类型稍有变化。
2.2.2、聊天室
制作一个简单的多人可聊天的聊天室。
Log.hpp
#pragma once#include <iostream>
#include <stdarg.h> // 使用可变参数列表需要用到这个头文件
#include <ctime>
#include <cstdlib>#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define SIZE 1024#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen; // 默认为屏幕打印path = "./log/";}void Enable(int method) // 更换日志的打印方式{printMethod = 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);}~Log(){}void operator()(int level, const char *format, ...){char leftbuffer[1024]; // 一条日志左边的格式信息,包括日志等级和时间。time_t t = time(nullptr); // 返回值是时间戳struct tm *ctime = localtime(&t); // 该函数可以将时间戳转换成一个struct tm 结构。// 下面的\是续行符,加不加都行。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);// printf("%s", logtxt);printLog(level, logtxt); // 打印}private:int printMethod; // 日志打印的方式std::string path; // 日志打印路径
};
Main.cc
#include "UdpServer.hpp"
#include "Log.hpp"#include <memory>
#include <cstdio>
#include <vector>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}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();return 0;
}
UdpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <string.h>
#include <functional>
#include <unordered_map>#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Log.hpp"typedef std::function<std::string(const std::string&, const std::string&, uint16_t)> func_t;Log lg;enum{SOCKET_ERR = 1, // socket创建失败BIND_ERR // 绑定失败
};std::string defaultip = "0.0.0.0"; // 任意地址绑定
uint16_t defaultport = 19000; // 默认端口号
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、创建socket,UDP的socket是全双工的,是允许同时被读写的。原因是分别有接收和发送的缓冲区。sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 注:AF_INET也可以写成PF_INET,两者值相同。if (sockfd_ < 0){lg(Fatal, "socket creation failed: %d, error string: %s", errno, strerror(errno));exit(SOCKET_ERR);}lg(Info, "socket created successfully: %d", sockfd_);// 2、绑定端口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()); // 将字符串转换成uint32_t类型,再将uint32_t转化为网络字节序IP地址。 //local.sin_addr.s_addr = INADDR_ANY; // 绑定任意地址if (bind(sockfd_, (const struct sockaddr*)&local, sizeof(local)) < 0){lg(Fatal, "socket bind failed: %d, error string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "socket binded successfully");}void CheckUser(const std::string info, const struct sockaddr_in& client, const std::string clientip, uint16_t clientport){auto it = online_user_.find(clientip);if(it == online_user_.end()){online_user_.insert({clientip, client});std::cout << "[" << clientip << ":" << clientport << "] add to online user" << std::endl;}if(info == "quit"){online_user_.erase(clientip);std::cout << "[" << clientip << ":" << clientport << "] delete to online user" << std::endl;}}void Broadcast(const std::string& info, const std::string clientip, uint16_t clientport){for(auto& user : online_user_){if(user.first == clientip) continue; // 不给自己发消息std::string message = "[";message += clientip;message += ":";message += std::to_string(clientport);message += "]# ";message += info;socklen_t len = sizeof(user.second);sendto(sockfd_, message.c_str(), message.size(), 0, (const sockaddr*)(&user.second), len);}} void Run() // 启动服务器{isrunning_ = true;char inbuffer[size];while (isrunning_){// 3、接收数据memset(inbuffer, 0, sizeof(inbuffer));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: %d, error string:%s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);std::string clientip = inet_ntoa(client.sin_addr);std::string info = inbuffer;CheckUser(info, client, clientip, clientport);if(info == ""){continue;}Broadcast(info, clientip, clientport);}}~UdpServer() // 释放资源{if(sockfd_ > 0) close(sockfd_); // 关闭网络文件描述符,其实一般也不需要。}private:int sockfd_; // 网络文件描述符std::string ip_; // IP地址uint16_t port_; // 端口号bool isrunning_; // 服务器运行状态std::unordered_map<std::string, struct sockaddr_in> online_user_; // 客户端信息映射表
};
UdpClient.cc
#include <iostream>
#include <cstdint>
#include <pthread.h>
#include <string.h>#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include <strings.h>using namespace std;void Usage(std::string proc) // 使用说明
{cout << "\n\rUsage: " << proc << " severip severport\n" << endl;
}struct ThreadData // 线程数据结构体
{struct sockaddr_in server; // 服务器地址信息int sockfd; // 网络文件描述符std::string serverip; // 服务器IP地址
};void* recv_message(void* arg) // 接收消息线程函数
{ThreadData* td = static_cast<ThreadData*>(arg);char buffer[1024];// 接收数据while(true){memset(buffer, 0, sizeof(buffer));struct sockaddr_in temp;socklen_t len = sizeof(temp);// 接收数据ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);if(s > 0){buffer[s] = 0;cout << '\r' << buffer << endl;cout << "Please Enter@ " << flush; }}
}void* send_message(void* arg) // 发送消息线程函数
{ThreadData* td = static_cast<ThreadData*>(arg);string message;socklen_t len = sizeof(td->server);sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), len); // 测试代码// 发送数据while(true){cout << "Please Enter@ " << flush;getline(cin, message);if(message == "quit"){sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), len);exit(0);}// 发送数据sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);}
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct ThreadData td;bzero(&td.server, sizeof(td.server));td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport);td.server.sin_addr.s_addr = inet_addr(serverip.c_str());td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (td.sockfd < 0){cout << "socker error" << endl;return 1;}td.serverip = serverip;pthread_t revcr, sender;pthread_create(&revcr, nullptr, recv_message, &td);pthread_create(&sender, nullptr, send_message, &td);pthread_join(revcr, nullptr);pthread_join(sender, nullptr);close(td.sockfd);return 0;
}