目录
和windows通信
引入
思路
WSADATA
代码
运行情况
简单的聊天室
思路
重定向
代码
terminal.hpp -- 重定向函数
服务端
客户端
运行情况
和windows通信
引入
linux和windows都需要联网,虽然他们系统设计不同,但网络部分一定是相同的,所以套接字也是一样的
- 这里我们只需要写出windows风格的客户端即可,服务端仍然在linux上跑
- 当然,除去套接字的部分,他们使用的接口和规则肯定是有区别的
思路
套接字的部分不变,处理一下头尾即可
首先要引入winsock2.h头文件,并引入库文件
定义一个WSADATA结构并初始化(不同版本,看到的接口+底层代码也不同)
WSADATA
- 用于在 Windows 操作系统上开发网络应用程序时管理套接字(sockets)库的初始化和配置
- 包含了关于 Winsock 环境的信息,例如 Winsock 版本、所支持的特性等
- 使用WSAStartup初始化,WSACleanup来释放资源并并终止 Winsock 环境
修改完之后,就可以让linux和windows通信了
代码
这里用的是vs2019,加了两个define,防止报错(vs太安全了,汗)
可以看出来,中间的socket+收发数据绝大部分都是一样的,只有那么一两个类型的命名不同:
#define _CRT_SECURE_NO_WARNINGS #define _WINSOCK_DEPRECATED_NO_WARNINGS #include <stdio.h> #include <winsock2.h> #include <Windows.h> #include<iostream> #include<string> #pragma comment(lib,"ws2_32.lib") //引入库文件 int main() { //初始化网络环境 WSADATA wsa; if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { printf("WSAStartup failed\n"); return -1; } //建立一个udp的socket SOCKET socked = socket(AF_INET, SOCK_DGRAM, 0); if (socked == INVALID_SOCKET) { printf("create socket failed\n"); return -1; } int port = 8080; std::string ip = "47.108.135.233"; //创建结构体 sockaddr_in addr = { 0 }; addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.S_un.S_addr = inet_addr(ip.c_str()); std::string info; char buffer[1024]; memset(buffer, 0, sizeof(buffer)); //收发数据 while (true) { std::cout << "Please enter:"; std::getline(std::cin, info); //发送数据 int n = sendto(socked, info.c_str(), info.size(), 0, (SOCKADDR*)&addr, sizeof(SOCKADDR)); if (n == 0) { printf("send failed\n"); return -1; } sockaddr_in t = { 0 }; int len = sizeof(sockaddr_in); // 接收数据 n = recvfrom(socked, buffer, sizeof(buffer) - 1, 0, (SOCKADDR*)&t, &len); buffer[n] = 0; std::cout << buffer << std::endl; memset(buffer, 0, sizeof(buffer)); } //关闭SOCKET连接 closesocket(socked); //清理网络环境 WSACleanup(); return 0; }
运行情况
我们成功在windows终端上与在linux下的服务端进行通信:
简单的聊天室
前面写的echo版就已经有聊天室的影子了,聊天软件的服务器的作用也就是向用户转发消息
这里我们修改一下就差不多了
思路
这里以ip地址和端口号作为每个人的标识(类似于用户名的作用),在echo版里其实就已经实现过了
但是echo版每个客户端的消息都是独立的
- 聊天室的话,每个人在自己的客户端上都可以看见彼此发出的消息
- 就需要我们将每条消息发送给所有运行起来的客户端
- 可以考虑创建一个在线用户表(ip,结构体对象) -- 每收到一个消息,就转发给所有注册在表中的用户
- 如果有登录功能的话,应该是登录后转发
- 这里稍微模拟一下登录过程 -- 当客户端运行起来后,有一句打印,且直接将该条打印语句发送给服务器,并且直接注册在表中(简易版嘛)
服务器修改好后,客户端就出现问题了
- 还记得我们的客户端吗,它的第一个函数就是getline
- 如果不发送消息的话,就会卡在那里不会往下走,也就无法调用下面的recvfrom函数,也就无法看见其他用户发送的数据
- 而udp协议是全双工的(它支持边读边写)
- 所以我们可以将客户端修改为多线程,一个读,一个写,这样就互不干扰了
虽然解决了收发消息的问题,但是客户端仅有一个窗口,这样直接打印的话,会导致输入消息和输出的消息混在一块
- 而聊天室一般是分为上下两部分,上面是所有人发送的消息,下面是自己的输入框
- 综合我们是在终端上显示,可以开俩终端,拼接在一起作为我们的界面,输入和输出在不同终端上工作
- 实现的话 -- /dev/pts里是终端文件
- 当我们打开xshell:
- 如果再开一个会话:
重定向
我们如果试着将数据重定向(dup2函数)到终端文件里,就可以看见自己的终端显示出了数据:
这样,我们就可以通过重定向,先确定当前终端属于哪个文件
然后就可以利用这个(也就是将数据重定向到终端文件里,而不是显示器),实现聊天室的分块显示
但如果输出数据时将fd=1的显示器重定向到终端文件1里,那么输入数据时/其他时候的打印,都会到那个终端文件里,而不会像我们预想的那样分成两个模块
所以,我们将标准错误重定向到其中一个终端文件里,另一个终端运行客户端,这样cout时会默认打印到当前终端里,就不会互相影响了
重定向既可以在代码中使用dup2函数,也可以直接在命令行中重定向(a>b)
代码
terminal.hpp -- 重定向函数
#include <iostream> #include <string> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> std::string terminal = "/dev/pts/2"; void my_dup() { int fd = open(terminal.c_str(), O_WRONLY); if (fd < 0) { perror("open"); exit(1); } dup2(fd, 2); close(fd); }
服务端
这里增加了用户表和chat函数(聊天室专用启动函数)
#include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <strings.h> #include <cstring> #include <string> #include <functional> #include <map> #include <iostream> #include "Log.hpp" extern std::string get_time(); Log lg; const int buff_size = 1024; using func_t = std::function<std::string(const std::string &)>; enum { SOCKET_ERR = 1, BIND_ERR = 2 }; // 启动服务器时,传入ip地址和端口号 // 手动启动 class udp_server { public: udp_server(const uint16_t port = 8080, const std::string ip = "0.0.0.0") : ip_(ip), port_(port), sockfd_(0) { } void run(func_t func) { init(); // 开始收发数据 char buffer[buff_size]; std::string message; while (true) { memset(buffer, 0, sizeof(buffer)); struct sockaddr_in src_addr; socklen_t src_len = sizeof(src_addr); // 获取数据 ssize_t n = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&src_addr), &src_len); if (n < 0) { lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno)); continue; } buffer[n] = 0; std::string id = generate_id(inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port)); message = id + "sever recvfrom success"; lg(INFO, message.c_str()); // 处理数据 std::string echo_info = func(buffer); // 响应给发送端 sendto(sockfd_, echo_info.c_str(), echo_info.size(), 0, reinterpret_cast<const struct sockaddr *>(&src_addr), src_len); message = id + "sever sendto success"; lg(INFO, message.c_str()); } } void chat() { init(); // 开始收发数据 char buffer[buff_size]; memset(buffer, 0, sizeof(buffer)); std::string message; while (true) { struct sockaddr_in src_addr; socklen_t src_len = sizeof(src_addr); // 获取数据 ssize_t n = recvfrom(sockfd_, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&src_addr), &src_len); char ip[30]; // std::cout << inet_ntop(AF_INET, &(src_addr.sin_addr), ip, sizeof(ip) - 1)<<std::endl; if (n < 0) { lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno)); continue; } buffer[n] = 0; // std::cout << buffer << std::endl; usr_[src_addr.sin_addr.s_addr] = src_addr; // 注册用户表 // for (auto it : usr_) // { // std::cout << inet_ntop(AF_INET, &((it.second).sin_addr), ip, sizeof(ip) - 1) << std::endl; // } std::string id = generate_id(inet_ntoa(src_addr.sin_addr), ntohs(src_addr.sin_port)); message = id + "sever recvfrom success"; lg(INFO, message.c_str()); // 处理数据 std::string time_stamp = get_time(); std::string echo_info = id + time_stamp + buffer; memset(buffer, 0, sizeof(buffer)); // 响应给所有用户端 send_all(echo_info); message = id + "sever sendto success"; lg(INFO, message.c_str()); } } ~udp_server() { if (sockfd_ > 0) { close(sockfd_); } } static std::string get_id() { udp_server obj; return obj.generate_id(obj.ip_, obj.port_); } private: std::string generate_id(const std::string ip, const uint16_t port) { return "[" + ip + ":" + std::to_string(port) + "]"; } void init() { // 创建套接字文件 sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd_ < 0) { lg(FATAL, "socket create error, sockfd : %d,%s", sockfd_, strerror(errno)); exit(SOCKET_ERR); } // 创建sockaddr结构 struct sockaddr_in addr; socklen_t len = sizeof(addr); bzero(&addr, len); addr.sin_addr.s_addr = inet_addr(ip_.c_str()); addr.sin_family = AF_INET; addr.sin_port = htons(port_); // 绑定套接字信息 int res = bind(sockfd_, reinterpret_cast<const struct sockaddr *>(&addr), len); if (res < 0) { lg(FATAL, "bind error, sockfd : %d,%s", sockfd_, strerror(errno)); exit(BIND_ERR); } lg(INFO, "bind success, sockfd : %d", sockfd_); } void send_all(const std::string &echo_info) { char ip[30]; for (auto it : usr_) { // std::cout << inet_ntop(AF_INET, &((it.second)->sin_addr), ip, sizeof(ip) - 1)<<std::endl; sendto(sockfd_, echo_info.c_str(), echo_info.size(), 0, reinterpret_cast<const struct sockaddr *>(&(it.second)), sizeof(it.second)); } } private: int sockfd_; std::string ip_; uint16_t port_; std::map<in_addr_t, struct sockaddr_in> usr_; //不能是指针,这样下次循环时,指针就换成新的客户端了 };
客户端
分出了写函数和读函数,chat函数中创建两个线程,让他们运行
#include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <arpa/inet.h> #include <functional> #include <strings.h> #include <cstring> #include <string> #include <iostream> #include <pthread.h> #include "Log.hpp" #include "terminal.hpp" const int buff_size = 1024; Log lg; enum { SOCKET_ERR = 1, BIND_ERR = 2 }; // 客户端需要提前知道服务端的套接字地址信息 // 日常生活中,我们一般直接通过网址进入,网址就是ip地址,且它会直接和端口号绑定 // 所以,这里我们只能自己手动提供服务端的ip和端口号 // 客户端不需要手动创建套接字,os会自动为我们提供(在首次发送数据时) struct data { int sockfd_; struct sockaddr_in *paddr_; socklen_t len_; }; class udp_client { public: udp_client(const uint16_t port = 8080, const std::string ip = "47.108.135.233") : ip_(ip), port_(port), sockfd_(0) { } void run() { data *d = init(); std::string info; char buffer[buff_size]; memset(buffer, 0, sizeof(buffer)); while (true) { std::cout << "Please enter:"; std::getline(std::cin, info); // 将消息发送给服务器 sendto(d->sockfd_, info.c_str(), info.size(), 0, reinterpret_cast<const struct sockaddr *>(d->paddr_), d->len_); info.clear(); struct sockaddr_in addr; // 仅用于填充参数,拿到自己的地址信息没啥意义 socklen_t len = sizeof(addr); // 获取数据 ssize_t n = recvfrom(d->sockfd_, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&addr), &len); if (n < 0) { lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno)); continue; } buffer[n] = 0; std::cout << buffer << std::endl; memset(buffer, 0, sizeof(buffer)); } } void chat() { data *d = init(); pthread_t r = 0, w = 0; pthread_create(&r, nullptr, input, d); pthread_create(&w, nullptr, output, d); pthread_join(r, nullptr); pthread_join(w, nullptr); } private: data *init() { // 创建套接字文件 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { lg(FATAL, "socket create error, sockfd : %d", sockfd); exit(SOCKET_ERR); } // 创建sockaddr结构 struct sockaddr_in *svr_paddr = new sockaddr_in; socklen_t svr_len = sizeof(*svr_paddr); bzero(svr_paddr, svr_len); inet_aton(ip_.c_str(), &(svr_paddr->sin_addr)); svr_paddr->sin_family = AF_INET; svr_paddr->sin_port = htons(port_); return new data({sockfd, svr_paddr, svr_len}); } static void *input(void *args) { data *d = reinterpret_cast<data *>(args); char ip[30]; inet_ntop(AF_INET, &((d->paddr_)->sin_addr), ip, sizeof(ip) - 1); std::string welcome = "comming..."; sendto(d->sockfd_, welcome.c_str(), welcome.size(), 0, reinterpret_cast<const struct sockaddr *>(d->paddr_), d->len_); std::string info; while (true) { std::cout << "Please enter:"; std::getline(std::cin, info); // 将消息发送给服务器 sendto(d->sockfd_, info.c_str(), info.size(), 0, reinterpret_cast<const struct sockaddr *>(d->paddr_), d->len_); info.clear(); } return nullptr; } static void *output(void *args) { data *d = reinterpret_cast<data *>(args); // my_dup(); char buffer[buff_size]; memset(buffer, 0, sizeof(buffer)); while (true) { struct sockaddr_in addr; // 仅用于填充参数,拿到自己的地址信息没啥意义 socklen_t len = sizeof(addr); // 获取数据(所有用户的消息都会获取) ssize_t n = recvfrom(d->sockfd_, buffer, sizeof(buffer) - 1, 0, reinterpret_cast<struct sockaddr *>(&addr), &len); if (n < 0) { lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno)); continue; } buffer[n] = 0; std::cerr << buffer << std::endl; memset(buffer, 0, sizeof(buffer)); } return nullptr; } private: int sockfd_; std::string ip_; uint16_t port_; };
两个cpp函数之间构建相应cs的对象+调用chat函数即可
运行情况
手动重定向(这个适合在其他主机上运行客户端,因为每个人打开的终端不一定正好有2,测试后进行手动重定向最好)
在代码内重定向:
下图是两个云服务器之间进行通信:
大家也可以下载文件试试,只要有两个执行文件+client文件执行时进行手动重定向(分好两个终端屏幕,确定好各自的编号),就能通信
(也就是说总共需要运行三个终端)