Linux —— Socket编程(二)
一、本篇重点
对于上一篇实现的简单udp客户端/服务器进一步的补充改造,继续了解与Socket api的相关接口
二、upd服务器(第一版)
上一篇我们通过一边实现一个简单udp,一边学习Socket编程的相关接口,本篇打算进一步对上一篇的代码进行补充和添加功能,上一篇为了了解接口,只是简单的让client端能够发送消息到server端,然后server端获取消息并能够发送回来即可,这里先提供完整的代码。
1. udp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include "errno.hpp"
#include <cstring>
namespace chk
{
const static uint16_t default_port = 8080;
class UdpServer
{
public:
// 对成员变量完成初始化
UdpServer(uint16_t port = default_port) : _port(port)
{
std::cout << "server port: " << _port << std::endl;
}
void Init() // 创建出套接字,并绑定端口号和ip
{
// 1. 创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock < 0)
{
std::cerr << "create socket error: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "bind socket success: " << _sock << std::endl; // 3
// 2. 构建struct sockaddr_in结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 初始化
local.sin_family = AF_INET; // IPv4
local.sin_port = htons(_port); // 端口号
local.sin_addr.s_addr = INADDR_ANY; // 服务器下的ip
// 3. 绑定套接字和sockaddr_in
int n = bind(_sock, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
std::cerr << "bind error: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket success: " << _sock << std::endl;
}
void Start() // 时刻读取套接字中的信息数据,并将其返回
{
char buffer[1024];
while(true)
{
// 收数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int n = recvfrom(_sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(n>0) buffer[n] = '\0';
else continue;
//收到信息后打印出来:对方ip+端口号+内容
std::cout << inet_ntoa(peer.sin_addr) << " - " << ntohs(peer.sin_port) << " : " << buffer << std::endl;
// 发回去
sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,sizeof(peer));
}
}
~UdpServer() // 析构
{
}
private:
int _sock;
uint16_t _port;
};
}
2. udp_server.cc
#include"udp_server.hpp"
#include<string>
#include<memory>
#include<cstdio>
using namespace std;
using namespace chk;
// 我们最终希望以 ./udp_server port 的形式去启动
static void usage(string proc)//使用手册
{
std::cout << "Usage:\n\t" << proc << " port\n" << std::endl;
}
int main(int argc,char* argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<UdpServer> usvr(new UdpServer(port));
usvr->Init();
usvr->Start();
return 0;
}
3. udp_client.hpp
#pragma once
#include<iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<cstring>
#include<string>
#include"errno.hpp"
4. udp_client.cc
#include"udp_client.hpp"//基本要用到的头文件
using namespace std;
static void usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}
// ./udp_client server_ip server_port
int main(int argc,char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
//1. 创建套接字
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0)
{
cerr << "client : create socket error" << endl;
exit(SOCKET_ERR);
}
//2. 创建server端的struct sockaddr
struct sockaddr_in server;
memset(&server,0,sizeof(server));//初始化方案2
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
server.sin_port = htons(server_port);
//3. 客户端测试
while(true)
{
// 用户发送消息
string messages;
cout << "client : " ;
cin >> messages;
// 发送到sock
sendto(sock,messages.c_str(),messages.size(),0,(struct sockaddr*)&server,sizeof(server));
// 接受返回的信息
char buffer[1024];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int n = recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
if(n > 0)
{
buffer[n] = 0;
cout << buffer << endl;
}
}
return 0;
}
5. errno.hpp
#pragma once
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
};
6. makefile
.PHONY:all
all: udp_client udp_server
udp_client:udp_client.cc
g++ -o $@ $^ -std=c++11
udp_server:udp_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udp_client udp_server
三、udp服务器(第二版)
1. 添加回调函数
第一版的udp服务器,我们只要求能够收发信息,但我们实际应用中,服务端接受到信息以后,还需要对信息进行后续的处理,而具体的处理方法,我们这里简单设计几个例子。
所以我们应该对udp_server.hpp的设计进行补充和改进,我们对类的设计可以多添加一个表示具体执行方法的成员,由外部传入具体的方法,类内只需要拿到方法后,以回调的方式去处理接受到的信息,并且将处理好的信息发送回给客户端。
———————————————————————————————————————————
知识点一
在c语言中,通常是用typedef的方式去定义一个函数指针类型,而在C++中,例如我想定义一个类型string fun_t(string msg) 这样一个类型的函数指针,我们通常使用以下方式:
#include <functional>
using func_t = std::function<std::string(std::string)>;
ps:这里用到的方法细究的话,知识点有点多,可以先简单的认为,这就是定义一个函数指针的方法,实际用处更加广泛,这个本质也不是函数指针。
———————————————————————————————————————————
基于上述在知识点,我们定义函数指针,添加函数指针变量的类成员对象,然后调整下构造,选择让外部传入具体方法,这样就实现了方法处理和网络传输的解耦,然后再对于之前的逻辑进行调整,我们拿到数据后,先将数据交给回调函数,然后将处理好后的信息再发生给客户端
2. 信息处理函数
这里我们简单的写几个功能进行测试即可
2.1 大小写转换
先简单实现一个,将对方发送过来的字符串信息中,关于小写的字母转换成大写返回
———————————————————————————————————————————
知识点一
这里是整理了一下要实现这个函数的一些接口,不算新的知识点
字符检查相关接口
#include<ctype.h>
int isalpha(int c); // 检查是否为字母
int islower(int c); // 检查是否为小写字母
int isupper(int c); // 检查是否为大写字母
英文字符大小写转化的相关接口
#include <ctype.h>
int toupper(int c); //小写转化成大写
int tolower(int c); //大写转化成小写
———————————————————————————————————————————
代码参考
string transacationString(string msg)
{
string res;
char tmp;
for(auto& c:msg)
{
if(islower(c))
{
tmp = toupper(c);
res.push_back(tmp);
}
else
{
res.push_back(c);
}
}
return res;
}
2.2 远程指令控制
这里要做一个让远程的客户端传指令给服务器,服务器处理并将结果返回,我们可以像之前一样,去封装一个命令行解释器,调用创建线程去处理并将结果返回,这里不选择自己造轮子,而是通过已有的接口去直接实现这个命令的处理,并返回相对应的处理结果。
———————————————————————————————————————————
知识点一:命令行执行接口
FILE *popen(const char *command, const char *type);
作用:创建一个子进程去执行command命令,并且创建管道连接这个子进程,通过type指定去连接到这个指令的标准输出或者标准输入
command
:要执行的 shell 命令字符串。type
:指定管道的方向,可以是"r"
(读)或"w"
(写)。如果是"r"
,则创建的管道连接到命令的标准输出,可以从这个管道读取命令的输出。如果是"w"
,则连接到命令的标准输入,可以向这个管道写入数据作为命令的输入。- 如果成功,返回一个指向
FILE
类型的指针,可以使用标准 I/O 函数(如fread
、fwrite
、fgets
等)来与管道进行交互。- 如果失败,返回
NULL
,并设置errno
来指示错误原因。
int pclose(FILE *stream);
作用:pclose
函数用于关闭由popen
函数创建的管道,并等待与该管道关联的进程结束。
stream
:由popen
函数返回的指向管道的FILE
指针。
———————————————————————————————————————————
参考代码
// 远程指令
std::string excuteCommand(std::string command)
{
//1. 安全检查:避免对方输入一些你不愿意提供的指令,例如rm等等
//这部分我们只是简单做测试,就不进行安全检查了
//2. 业务处理逻辑
FILE* fp = popen(command.c_str(),"r");
if(fp == nullptr) return "Invalid instruction";
//3. 获取结果
char line[1024];
std::string res;
while(fgets(line,sizeof(line),fp))
{
res += line;
}
pclose(fp);
return res;
}
四、udp服务器(第三版)
服务端除了这种对信息的处理然后返回,我们还可以简单的做一个群聊功能玩玩。
群聊的要求就是,我们多个客户端向服务器发送消息,服务器首先需要每个都拿到,并且将受到的消息,广播给每一个在线用户,要实现这些,首先要解决的就是,我需要将消息发送到每一个客户端上,我就要有一个存放每一个在线用户信息的一个容器,这里做的稍微简单一点,我们认为只要给我发送了消息,我就将你的客户端信息记录起来,认为你当前处于“在线状态”,然后将你的信息向每一个用户进行发送,此时如果有其他用户也给服务端发送信息怎么办?这就涉及到多线程并发访问的问题,我们利用前面学习到的生产消费模型去处理,多个线程发送消息到循环队列中,而服务器每次往循环队列拿信息,并且广播。而为了测试时观赏性更强一点,我们还可以利用管道文件,将内容输入重定向到管道文件中,再另外开一个窗口,把管道文件的内容打印出来。
参考代码
Linux —— udp实现群聊代码-CSDN博客
总结
本篇重点是延续上一篇,进一步改造了udp服务端,简单的补充和添加了几个功能,并且提供了代码进行参考。