目录
第一版:
common
服务端:
客户端
第一版问题总结:
第二版
服务端:
客户端:
改进:
Windows客户端
一些小问题
还可以进行的改进
这篇文章我就先不讲网络基础的东西了,我讲讲在我进行制作我这个拉跨聊天室中遇到的问题,并写了三版代码.
第一版:
common
#pragma once
#include <iostream>
#include <fstream>
#include <cstdio>
#include <string>
#include <ctime>
#include <cstdarg>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "LockGuard.hpp"
enum
{
SOCKET_ERROR = 1,
BIND_ERROR,
USAGE_ERROR
};
void Setserver(struct sockaddr_in &server,std::string &serverip,uint16_t &serverport)
{
bzero(&server, sizeof(server));
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
server.sin_family=AF_INET;
}
class InetAddr
{
void GetAddress(uint16_t *Port, std::string *Ip)
{
*Port = ntohs(_Addr.sin_port);
*Ip = inet_ntoa(_Addr.sin_addr);
}
public:
InetAddr(struct sockaddr_in &Addr)
: _Addr(Addr)
{
GetAddress(&_Port, &_Ip);
}
uint16_t Port()
{
return _Port;
}
std::string Ip()
{
return _Ip;
}
private:
struct sockaddr_in _Addr;
uint16_t _Port;
std::string _Ip;
};
服务端:
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
#include "Common.hpp"
const static int defaultfd = -1;
class Udpserver
{
public:
Udpserver(uint16_t port)
: _socketfd(defaultfd), _prot(port), _isrunning(false)
{
}
void InitServer()
{
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd < 0)
{
LOG(WARNING, "%s", "sockfd创建失败");
}
LOG(INFO, "%s", "sock创建成功");
// 填充sockaddr_in结构
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET; // 设置家族协议
local.sin_port = htons(_prot); // 设置端口号 换成网络序列
// port要经过网络传输给对面,先到网络,_port:主机序列-> 主机序列,转成网络序列
// a. 字符串风格的点分十进制的IP地址转成 4 字节IP
// b. 主机序列,转成网络序列
// in_addr_t inet_addr(const char *cp) -> 同时完成 a & b
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // "192.168.3.1" -> 字符串风格的点分十进制的IP地址 -> 4字节IP
local.sin_addr.s_addr = INADDR_ANY; // 随机ip地址 一般不能绑定确定ip地址
// 开始绑定
int n = bind(_socketfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(FATAL, "%s", "bind error");
exit(BIND_ERROR);
}
LOG(INFO, "%s", "bind success");
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char Inbuffer[1024];
struct sockaddr_in peer;
socklen_t socklen = sizeof(peer); // 初始化为sock
// 让server收取数据 获取客户端socket
ssize_t n = recvfrom(_socketfd, Inbuffer, sizeof(Inbuffer), 0, (struct sockaddr *)&peer,&socklen); // 接受
if (n > 0)
{
std::cout <<"client say:";
std::cout << Inbuffer << std::endl; // 成功就打印出来
// sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
LOG(DEBUG, "建立 客户IP:%s 连接端口:%s", netAddr.Ip().c_str(), netAddr.Port());
// sendto(_socketfd,buffer,sizeof(buffer),0,(struct sockaddr*)&sockaddr_in,socklen);//发送
}
//
///发/
//
InetAddr netAddr(peer);
std::string message;
std::cout << "server:";
getline(std::cin, message);
sendto(_socketfd, message.c_str(), message.size(), 0, (struct sockaddr *)&peer, socklen); // 发送
}
_isrunning = false;
}
private:
int _socketfd;
uint16_t _prot;
bool _isrunning;
};
客户端
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
#include "Common.hpp"
static bool isrunning = false;
static std::string Clientname;
void useagge(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
useagge(argv[0]);
exit(USAGE_ERROR);
}
std::string serverip = argv[1];
uint16_t serverport = std::atoi(argv[2]);
std::cout << "你是?" << std::endl;
std::cin >> Clientname;
// 创建socket;
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
LOG(WARNING, "%s", "Sock错误创建失败");
}
LOG(INFO, "%s", "sock创建成功");
isrunning = true;
struct sockaddr_in server;
Setserver(server, serverip, serverport);
struct sockaddr_in local;
bzero(&local, sizeof(local));
std::cout << "可以进行通信了!" << std::endl;
while (isrunning)
{
std::cout << "server:";
std::string message;
getline(std::cin, message);
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, socklen); // 发送
struct sockaddr_in peer;
socklen_t socklen = sizeof(peer); // 初始化为sock
char buffer[1024];
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &socklen);
if (n > 0)
{
buffer[n] = 0;
std::cout << "server: " << buffer << std::endl;
}
}
return 0;
}
第一版问题总结:
我一开始是想着这个流程,因为一开始服务端只是接受客户端,服务端不会发消息给客户端,所以我想在原基础上,让两端都可以接受和发送,当时就有想可以多线程实行接受和发的任务,但是觉得上线程太麻烦就决定是服务端发->客户端收->客户端发->服务器收,这一条链路实行,但是问题是,我把收发是写在循环里,而 recvfrom是非阻塞等待的,所以双方实际上永远等不到对方信息
所以实际上仍然是要让多线程实行接受和发的任务
第二版
服务端:
#include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <thread> #include <mutex> #include "log.hpp" #include "Common.hpp" const static int defaultfd = -1; public: Udpserver(uint16_t port) : _socketfd(defaultfd), _prot(port), _isrunning(false) { _socklen = sizeof(_peer); InitServer(); } void InitServer() { _socketfd = socket(AF_INET, SOCK_DGRAM, 0); if (_socketfd < 0) { LOG(WARNING, "%s", "sockfd创建失败"); } LOG(INFO, "%s", "sock创建成功"); // 填充sockaddr_in结构 struct sockaddr_in local; bzero(&local, sizeof(local)); local.sin_family = AF_INET; // 设置家族协议 local.sin_port = htons(_prot); // 设置端口号 换成网络序列 // port要经过网络传输给对面,先到网络,_port:主机序列-> 主机序列,转成网络序列 // a. 字符串风格的点分十进制的IP地址转成 4 字节IP // b. 主机序列,转成网络序列 // in_addr_t inet_addr(const char *cp) -> 同时完成 a & b // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // "192.168.3.1" -> 字符串风格的点分十进制的IP地址 -> 4字节IP local.sin_addr.s_addr = INADDR_ANY; // 随机ip地址 一般不能绑定确定ip地址 // 开始绑定 int n = bind(_socketfd, (struct sockaddr *)&local, sizeof(local)); if (n < 0) { LOG(FATAL, "%s", "bind error"); exit(BIND_ERROR); } LOG(INFO, "%s", "bind success"); _isrunning = true; } void Start() void receive() { _isrunning = true; while (_isrunning) { char Inbuffer[1024] = {0}; // 初始化缓冲区 struct sockaddr_in tempPeer; socklen_t tempSocklen = sizeof(tempPeer); ssize_t n = recvfrom(_socketfd, Inbuffer, sizeof(Inbuffer) - 1, 0, (struct sockaddr *)&tempPeer, &tempSocklen); if (n > 0) { std::cout <<"client say:"; std::cout << Inbuffer << std::endl; // 成功就打印出来 // sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len); LOG(DEBUG, "建立 客户IP:%s 连接端口:%s", netAddr.Ip().c_str(), netAddr.Port()); // sendto(_socketfd,buffer,sizeof(buffer),0,(struct sockaddr*)&sockaddr_in,socklen);//发送 Inbuffer[n] = 0; std::lock_guard<std::mutex> lock(_peerMutex); _peer = tempPeer; _socklen = tempSocklen; std::cout << "client says:" << Inbuffer << std::endl; } // ///发/ // InetAddr netAddr(peer); else { perror("recvfrom error"); } } } void sent() { while (_isrunning) { std::string message; std::cout << "server: "; std::cin >> message; std::lock_guard<std::mutex> lock(_peerMutex); ssize_t sent = sendto(_socketfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_peer, _socklen); if (sent == -1) { perror("sendto error"); } } _isrunning = false; } void Start() { std::thread recvThread(&Udpserver::receive, this); std::thread sendThread(&Udpserver::sent, this); recvThread.detach(); sendThread.detach(); while (_isrunning) { std::this_thread::sleep_for(std::chrono::seconds(1)); } close(_socketfd); } private: int _socketfd; uint16_t _prot; bool _isrunning; // struct sockaddr_in _peer; socklen_t _socklen; std::mutex _peerMutex; };
客户端:
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#include <thread>
#include "log.hpp"
#include "Common.hpp"
static bool isrunning = false;
static std::string Clientname;
class Client
{
public:
Client(const std::string &server_ip, uint16_t server_port)
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
perror("socket creation failed");
exit(EXIT_FAILURE);
}
memset(&_serverAddr, 0, sizeof(_serverAddr));
_serverAddr.sin_family = AF_INET;
_serverAddr.sin_port = htons(server_port);
if (inet_pton(AF_INET, server_ip.c_str(), &_serverAddr.sin_addr) <= 0)
{
perror("inet_pton failed");
close(_sockfd);
exit(EXIT_FAILURE);
}
_isrunning = true;
}
void start()
{
std::cout << "你是? ";
std::cin >> _clientName;
std::cout << "可以进行通信了!" << std::endl;
std::thread recvThread(&Client::receive, this);
std::thread sendThread(&Client::send, this);
recvThread.detach();
sendThread.detach();
while (_isrunning)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
}
close(_sockfd);
}
private:
void receive()
{
while (_isrunning)
{
char buffer[1024] = {0};
struct sockaddr_in peer;
socklen_t socklen = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &socklen);
if (n > 0)
{
buffer[n] = 0;
std::cout << "server: " << buffer << std::endl;
}
else if (n == -1)
{
perror("recvfrom error");
}
}
}
void send()
{
while (_isrunning)
{
std::cout << _clientName << ": ";
std::string message;
std::cin >> message;
ssize_t sent = sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_serverAddr, sizeof(_serverAddr));
if (sent == -1)
{
perror("sendto error");
}
}
}
private:
int _sockfd;
struct sockaddr_in _serverAddr;
bool _isrunning;
std::string _clientName;
};
改进:
这一版上,我添加了多线程和锁,能让客户端服务端进行并发的运行,并收发消息
Windows客户端
由于我想让Windows朋友也能与我建立通信,所以我在客户端上进行了修改成Windows版本
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <thread>
#include <memory>
#pragma comment(lib, "Ws2_32.lib")
static bool isrunning = false;
static std::string Clientname;
class Client
{
public:
Client(const std::string& server_ip, uint16_t server_port)
{
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0)
{
std::cerr << "WSAStartup failed: " << result << std::endl;
exit(EXIT_FAILURE);
}
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == INVALID_SOCKET)
{
std::cerr << "socket creation failed: " << WSAGetLastError() << std::endl;
WSACleanup();
exit(EXIT_FAILURE);
}
memset(&_serverAddr, 0, sizeof(_serverAddr));
_serverAddr.sin_family = AF_INET;
_serverAddr.sin_port = htons(server_port);
if (inet_pton(AF_INET, server_ip.c_str(), &_serverAddr.sin_addr) <= 0)
{
std::cerr << "inet_pton failed: " << WSAGetLastError() << std::endl;
closesocket(_sockfd);
WSACleanup();
exit(EXIT_FAILURE);
}
_isrunning = true;
}
void start()
{
std::cout << "你是? ";
std::cin >> _clientName;
std::cout << "可以进行通信了!" << std::endl;
std::thread recvThread(&Client::receive, this);
std::thread sendThread(&Client::send, this);
recvThread.detach();
sendThread.detach();
while (_isrunning)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
}
closesocket(_sockfd);
WSACleanup();
}
private:
void receive()
{
while (_isrunning)
{
char buffer[1024] = { 0 };
struct sockaddr_in peer;
int peerlen = sizeof(peer);
int n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &peerlen);
if (n > 0)
{
buffer[n] = 0;
std::cout << "server: " << buffer << std::endl;
}
else if (n == SOCKET_ERROR)
{
std::cerr << "recvfrom error: " << WSAGetLastError() << std::endl;
}
}
}
void send()
{
while (_isrunning)
{
std::cout << _clientName << ": ";
std::string message;
std::cin >> message;
int sent = sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&_serverAddr, sizeof(_serverAddr));
if (sent == SOCKET_ERROR)
{
std::cerr << "sendto error: " << WSAGetLastError() << std::endl;
}
}
}
private:
SOCKET _sockfd;
struct sockaddr_in _serverAddr;
bool _isrunning;
std::string _clientName;
};
void useagge(const std::string& proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
<< std::endl;
}
int main()
{
std::string serverip;
std::string portStr;
std::cout << "输入服务器ip: ";
std::cin >> serverip;
std::cout << "输入服务器端口号: ";
std::cin >> portStr;
uint16_t serverport;
try
{
serverport = static_cast<uint16_t>(std::stoi(portStr));
}
catch (const std::exception& e)
{
std::cerr << "无效的端口号: " << e.what() << std::endl;
return EXIT_FAILURE;
}
std::unique_ptr<Client> csvr = std::make_unique<Client>(serverip, serverport); // C++14
csvr->start();
return 0;
}
一些小问题
由于我用的云服务器,大部分端口号是默认禁用的,所以端口号要自己进行开放,我用的阿里云,开放端口的地方在这里
还可以进行的改进
1.目前服务端和客户端仍然是1对1的关系,如果有第二个用户上线,就会挤占第一个用户,所以这里可以用一个vector来对用户的ip进行管理,来统一收所有用户消息
2.目前还没有将用户的名字传输给服务端,后续可以加上