(。・∀・)ノ゙嗨!你好这里是ky233的主页:这里是ky233的主页,欢迎光临~https://blog.csdn.net/ky233?type=blog
点个关注不迷路⌯'▾'⌯
实现一个简单的对话发消息的功能!
目录
一.新增的接口:
1.socket
2.bing
3.inet_addr
4.recvform
5.inet_ntoa
5.sendto
6.popen
二、初步代码以及结果
1.udp_server.hpp
2.udp_server.cc
3.udp_client
4.log.hpp
5.结果
三、爆改多线程
1.udp_client.cc
2.udp_server.cc
3.udp_server.hpp
4.thread.hpp
5.结果
一.新增的接口:
1.socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- 参数一:叫做域,用来判断是那个类型的,AF_UNIX, AF_LOCAL(用于本地通信)AF_INET(用于ipv4的网络通信)
- 参数二:类型,通信的种类是什么,SOCK_DGRAM(套接字的类别,数据报的方式)
- 参数三:协议,比如用AF_INET,SOCK_DGRAM,一般直接写0
- 创建成功会得到一个文件描述符,失败返回-1
2.bing
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
- 参数一:创建的套接字
- 参数二:填充sockaddr结构体
- 参数三:所传入这个结构体对象的长度
3.inet_addr
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
n_addr_t inet_addr(const char *cp);
- 将点分十进制转换成四字节,并且将主机序列转换成网络序列
- 参数一:传入的ip
4.recvform
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- 用于从套接字接收数据。该函数通常与无连接的数据报服务
- 参数一:套接字
- 参数二和参数三:读取数据的缓冲区,用于存放读取到的数据,len叫做最终的大小
- 参数四:读取的方式,0为阻塞的方式读取
- 参数五和参数六:是输出型参数,我们还要知道是谁给我们发的消息
5.inet_ntoa
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
- 将点分四字节转换成点分十进制,并且将网络序列转换成主机序列
- 参数一:从网络中获取到的ip
5.sendto
#include <sys/types.h>
#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);
- 参数一:套接字
- 参数二和参数三:要发送的数据和长度
- 参数四:默认为0,阻塞
- 参数五:要把数据发给谁,
- 参数六:这个缓冲区的长度是多少
6.popen
#include <stdio.h>
FILE *popen(const char *command, const char *type);
二、初步代码以及结果
1.udp_server.hpp
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include "log.hpp"
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <cstdio>
#include <unistd.h>
#define SIZE 1024
class UDPServer
{
public:
UDPServer(uint16_t port,std::string ip="")
:port_(port),ip_(ip),sock_(-1)
{}
bool initServer()
{
//这里开始使用网络部分
//1.创建套接字
sock_=socket(AF_INET,SOCK_DGRAM,0);
if(sock_<0)
{
logMessage(FATAL,"%d:%s",errno,strerror(errno));
exit(2);
}
//2.bind绑定:将用户设置的ip和端口号在内核中和我们的进程强关联
//"192.168.1.0"->点分十进制
//以.来分隔,每个区域的取值范围为0——255,
//正好为1字节,四个区域理论上需要四字节
//所以我们的看的时候会由4字节显示,转换为点分十进制
//初始化结构体完成
struct sockaddr_in local;
bzero(&local,sizeof(local));
//填充结构体
local.sin_family=AF_INET;
//服务器的IP和端口号,未来也是要发送给对方主机才能互相通信的
//所以要先把数据发送到网络,要用htons
local.sin_port=htons(port_);
//同上要先把点分十进制转换成四字节ip
//还需要要把四字节主机序列,转换成网络序列
//所以用inet_addr一次性转成网络四字节序列
//local.sin_addr.s_addr=inet_addr(ip_.c_str());
//一般情况下,只要是这个端口的,那么我们就直接全部都接受,而不是指定端口
local.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str());
//这里需要做强转,因为bind需要的是sockaddr类型而不是sockaddr_in类型
if(bind(sock_,(struct sockaddr*)&local,sizeof(local))<0)
{
logMessage(FATAL,"%d:%s",errno,strerror(errno));
exit(2);
}
//至此初始化服务器写完,首先创建套接字,接着用bind来绑定ip和端口号写进内核中
logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
return true;
}
void strat()
{
//作为一款网络服务器,是永远不退出的!
// 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
//echo服务器,主机给我们发送消息,我们原封不动的返回
char buffer[SIZE];
for( ; ;)
{
//1.读取数据
//注意peer,纯输出型参数
struct sockaddr_in peer;
bzero(&peer,sizeof(peer));
//输入:peer 缓冲区大小
//输入:实际读到的peer大小
//所以len的大小要为实际的peer大小
socklen_t len = sizeof(peer);
//这样就把数据读取到buffer里了
ssize_t s= recvfrom(sock_,&buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s>0)
{
buffer[s]=0;//我们目前数据当作字符串
//1.输出发送的数据信息
//2.是谁?提取!
uint16_t cil_port=ntohs(peer.sin_port);//从网络中来的,所以要进行转成主机端口号
std::string cli_p=inet_ntoa(peer.sin_addr);//将点分四字节转换成点分十进制,并且将网络序列转换成主机序列
printf("[%s:%d]# %s\n",cli_p.c_str(),cil_port,buffer);
}
//2.分析和处理数据
//3.写回数据
sendto(sock_,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,len);
}
}
~UDPServer()
{
if(sock_>=0)
{
close(sock_);
}
}
private:
uint16_t port_;
std::string ip_;
int sock_;
};
#endif
2.udp_server.cc
#include "udp_server.hpp"
#include <memory>
#include <cstdlib>
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
int main(int argc,char *argv[])
{
//检查传入ip和端口号是否错误
if(argc!=2)
{
usage(argv[0]);
exit(1);
}
//初始化服务器,利用unique_ptr
uint16_t port=atoi(argv[1]);
//std::string ip=argv[1];
std::unique_ptr<UDPServer> svr(new UDPServer(port));
svr->initServer();
svr->strat();
return 0;
}
3.udp_client
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#include "thread.hpp"
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
//client要不要bind?要bind,但是client不会显示的bind,程序员不会自己bind
//client是一个客户端,是普通人下载并使用的
//如果需要显示的bind,也就要求了客户端也bind了一个固定的端口
//万一其他的客户端提前真用了端口号了呢,这时这个客户端就无法启动了
//所以不用显示的bind指定端口号,直接让OS自动随机选择
std::string message;
//给谁发的信息如下
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr=inet_addr(argv[1]);
char buffer[1024];
while(1)
{
std::cout<<"请输入你的信息# ";
std::getline(std::cin,message);
if(message=="quit")
{
break;
}
//当client首次发送消息给服务器的时候,OS会自动给client bind它的ip和port
sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
//至此客户端和服务器已经建成
//这里需要定义两个占位的
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s= recvfrom(sock,&buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
if(s>0)
{
buffer[s]=0;
std::cout<<"server echo# "<<buffer<<std::endl;
}
}
close(sock);
return 0;
}
4.log.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./threadpool.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
if(level== DEBUG) return;
#endif
// va_list ap;
// va_start(ap, format);
// while()
// int x = va_arg(ap, int);
// va_end(ap); //ap=nullptr
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// vprintf(format, args);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
//FILE *fp = fopen(LOGFILE, "a");
printf("%s%s\n", stdBuffer, logBuffer);
//fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
//fclose(fp);
}
5.结果
这个叫做本地环回:client和server发送数据只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中,通常用于本地网络服务器的测试
——————————————————————————————————————————
其中客户端时OS随机绑定的,全0IP是因为只要是这个端口的我都要,不要具体IP的
三、爆改多线程
我们要把这个代码改成一个类似QQ群一样的模式,所有人发的消息都可以看到
1.udp_client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#include "thread.hpp"
uint16_t serverport=0;
std::string serverip;
//发现:无论是多线程读还是写,用的sock都是一个,sock代表就是文件,UDP是全双工的-> 可以同时进行收发而不受干扰
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
<< std::endl;
}
//发送逻辑的回调
static void *udpSend(void *args)
{
//根据线程的封装,先转换成data*的然后获取里面的args_,然后在转会int*
//获得一个指针指向args里面的sock,然后解引用
int sock=*(int*)((ThreadData*)args)->args_;
std::string name = ((ThreadData*)args)->name_;
//下面逻辑与之前一样,构建发送的字符串,套接字信息
std::string message;
//给谁发的信息如下
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());
//开始不间断的发送
while(1)
{
std::cerr<<"请输入你的信息# ";
std::getline(std::cin,message);
if(message=="quit")
{
break;
}
//当client首次发送消息给服务器的时候,OS会自动给client bind它的ip和port
sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
//至此客户端和服务器已经建成
}
return nullptr;
}
//接受逻辑的回调
static void *udpRecv(void *args)
{
int sock=*(int*)((ThreadData*)args)->args_;
std::string name = ((ThreadData*)args)->name_;
//需要不断地去读,要去指定的套接字中读取
while(1)
{
char buffer[1024];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s= recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
if(s>0)
{
buffer[s]=0;
std::cout<<buffer<<std::endl;
}
}
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
serverport=atoi(argv[2]);
serverip=argv[1];
//client要不要bind?要bind,但是client不会显示的bind,程序员不会自己bind
//client是一个客户端,是普通人下载并使用的
//如果需要显示的bind,也就要求了客户端也bind了一个固定的端口
//万一其他的客户端提前真用了端口号了呢,这时这个客户端就无法启动了
//所以不用显示的bind指定端口号,直接让OS自动随机选择
//爆改多线程,一个线程发送数据到各个主机,一个线程接受各个数据。
std::unique_ptr<Thread> sender(new Thread(1,udpSend,(void*)&sock));
std::unique_ptr<Thread> recver(new Thread(2,udpRecv,(void*)&sock));
sender->start();
recver->start();
sender->join();
recver->join();
close(sock);
// std::string message;
// //给谁发的信息如下
// struct sockaddr_in server;
// memset(&server,0,sizeof(server));
// server.sin_family=AF_INET;
// server.sin_port=htons(atoi(argv[2]));
// server.sin_addr.s_addr=inet_addr(argv[1]);
// char buffer[1024];
// while(1)
// {
// std::cout<<"请输入你的信息# ";
// std::getline(std::cin,message);
// if(message=="quit")
// {
// break;
// }
// //当client首次发送消息给服务器的时候,OS会自动给client bind它的ip和port
// sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
// //至此客户端和服务器已经建成
// //这里需要定义两个占位的
// struct sockaddr_in temp;
// socklen_t len = sizeof(temp);
// ssize_t s= recvfrom(sock,&buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
// if(s>0)
// {
// buffer[s]=0;
// std::cout<<"server echo# "<<buffer<<std::endl;
// }
// }
// close(sock);
return 0;
}
2.udp_server.cc
#include "udp_server.hpp"
#include <memory>
#include <cstdlib>
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
int main(int argc,char *argv[])
{
//检查传入ip和端口号是否错误
if(argc!=2)
{
usage(argv[0]);
exit(1);
}
//初始化服务器,利用unique_ptr
uint16_t port=atoi(argv[1]);
//std::string ip=argv[1];
std::unique_ptr<UDPServer> svr(new UDPServer(port));
svr->initServer();
svr->strat();
return 0;
}
3.udp_server.hpp
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include "log.hpp"
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <cstdio>
#include <unistd.h>
#include <unordered_map>
#define SIZE 1024
class UDPServer
{
public:
UDPServer(uint16_t port, std::string ip = "")
: port_(port), ip_(ip), sock_(-1)
{
}
bool initServer()
{
// 这里开始使用网络部分
// 1.创建套接字
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_ < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
// 2.bind绑定:将用户设置的ip和端口号在内核中和我们的进程强关联
//"192.168.1.0"->点分十进制
// 以.来分隔,每个区域的取值范围为0——255,
// 正好为1字节,四个区域理论上需要四字节
// 所以我们的看的时候会由4字节显示,转换为点分十进制
// 初始化结构体完成
struct sockaddr_in local;
bzero(&local, sizeof(local));
// 填充结构体
local.sin_family = AF_INET;
// 服务器的IP和端口号,未来也是要发送给对方主机才能互相通信的
// 所以要先把数据发送到网络,要用htons
local.sin_port = htons(port_);
// 同上要先把点分十进制转换成四字节ip
// 还需要要把四字节主机序列,转换成网络序列
// 所以用inet_addr一次性转成网络四字节序列
// local.sin_addr.s_addr=inet_addr(ip_.c_str());
// 一般情况下,只要是这个端口的,那么我们就直接全部都接受,而不是指定端口
local.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str());
// 这里需要做强转,因为bind需要的是sockaddr类型而不是sockaddr_in类型
if (bind(sock_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
// 至此初始化服务器写完,首先创建套接字,接着用bind来绑定ip和端口号写进内核中
logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
return true;
}
void strat()
{
// 作为一款网络服务器,是永远不退出的!
// 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
// echo服务器,主机给我们发送消息,我们原封不动的返回
char buffer[SIZE];
for (;;)
{
// 1.读取数据
// 注意peer,纯输出型参数
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
// 输入:peer 缓冲区大小
// 输入:实际读到的peer大小
// 所以len的大小要为实际的peer大小
socklen_t len = sizeof(peer);
// 这样就把数据读取到buffer里了
ssize_t s = recvfrom(sock_, &buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
char result[256];
char key[64];
std::string cmd_echo;
if (s > 0)
{
buffer[s] = 0; // 我们目前数据当作字符串
// 1.输出发送的数据信息
// 2.是谁?提取!
// if(strcasestr(buffer,"rm")!=nullptr||strcasestr(buffer,"rmdir")!=nullptr)
// {
// std::string err_message="坏人";
// std::cout<<err_message<<buffer<<std::endl;
// sendto(sock_, err_message.c_str(), err_message.size(), 0, (struct sockaddr *)&peer, len);
// continue;
// }
// FILE *fp = popen(buffer, "r");
// if (nullptr == fp)
// {
// logMessage(ERROR, "%d:%s", errno, strerror(errno));
// continue;
// }
// while (fgets(result, sizeof(result), fp) != nullptr)
// {
// cmd_echo += result;
// }
// fclose(fp);
uint16_t cil_port = ntohs(peer.sin_port); // 从网络中来的,所以要进行转成主机端口号
std::string cil_ip = inet_ntoa(peer.sin_addr); // 将点分四字节转换成点分十进制,并且将网络序列转换成主机序列
// printf("[%s:%d]# %s\n",cli_p.c_str(),cil_port,buffer);
snprintf(key, sizeof(key), "%s-%d", cil_ip.c_str(), cil_port);
logMessage(NORMAL,"key: %s",key);
auto it = users_.find(key);
if(it==users_.end())
{
logMessage(NORMAL,"add new user:%s",key);
users_.insert({key,peer});
}
}
// 2.分析和处理数据
// 3.写回数据
// sendto(sock_,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,len);
//sendto(sock_, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr *)&peer, len);
for(auto &iter:users_)
{
std::string sendMessage=key;
sendMessage+="# ";
sendMessage+=buffer;
logMessage(NORMAL,"push message to %s",iter.first.c_str());
sendto(sock_, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
}
}
}
~UDPServer()
{
if (sock_ >= 0)
{
close(sock_);
}
}
private:
uint16_t port_;
std::string ip_;
int sock_;
std::unordered_map<std::string, struct sockaddr_in> users_;
};
#endif
4.thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>
using namespace std;
typedef void*(*fun_t)(void*);
class ThreadData
{
public:
void* args_;
string name_;
};
class Thread
{
public:
Thread(int num,fun_t callback,void* args):func_(callback)
{
char namebuffer[64];
snprintf(namebuffer,sizeof(namebuffer),"Thread-%d",num);
name_=namebuffer;
tdata_.args_=args;
tdata_.name_=name_;
}
void start()
{
pthread_create(&tid_,nullptr,func_,&tdata_.args_);
}
void join()
{
pthread_join(tid_,nullptr);
}
string name()
{
return name_;
}
~Thread()
{
}
private:
string name_;
fun_t func_;
ThreadData tdata_;
pthread_t tid_;
};
5.结果
利用管道可以更好的观看