小项目多人聊天室,主要用socket进行网络通信,使用花生壳内网穿透进行远程连接,使用dos窗口进行输入显示聊天记录
话不多数,直接进入正题(先整个目录)
linux下服务端
使用epoll进行多路i/o监听,收到某个客户端的消息后进行分发,使用map存储客户端信息,以下为代码
#include<iostream>
#include<string>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<map>
#include <arpa/inet.h>
const int MAX_CONN=1024;
using namespace std;
struct client{
int sockfd;
std::string daohao;//道友道号
};
int main(){
//创建epoll
int epld=epoll_create(100);
if(epld<0){
perror("epoll is error");
return -1;
}
//创建监听套接字
int listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd<0){
perror("listen is eror");
return -1;
}
//绑定地址
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=htonl(INADDR_ANY);
addr.sin_port=htons(6789);
int ret =bind(listenfd,(sockaddr*)&addr,sizeof(addr));
if(ret<0){
perror("bind is error");
return -1;
}
// 保存客户端信息
map<int,client>conn_clients;
//开始监听
ret = listen (listenfd,1024);
if(ret<0){
perror("listen is error");
return -1;
}
//加入epoll
struct epoll_event l_epoll;
l_epoll.events = EPOLLIN;
l_epoll.data.fd=listenfd;
ret = epoll_ctl(epld,EPOLL_CTL_ADD,listenfd,&l_epoll);
if(ret<0){
perror("epoll_ctl is error");
return -1;
}
//循环监听
while(1){
struct epoll_event c_epoll[MAX_CONN];//监听到有数据的fd放的地方
int n=epoll_wait(epld,c_epoll,MAX_CONN,-1);
if(n<0){
perror("epoll_wait is error");
return -1;
}
for(int i=0;i<n;i++){
int fd=c_epoll[i].data.fd;
if(fd==listenfd){
//有人连进来了
struct sockaddr_in client_addr;
socklen_t client_len=sizeof(client_addr);
int clientfd=accept(listenfd,(sockaddr*)&client_addr,&client_len);
//打印输出接入客户端
char clientIP[16];
inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,clientIP,sizeof(clientIP));
unsigned short clientport =ntohs(client_addr.sin_port);
cout<<"ip:"<<clientIP<<" "<<"端口"<<clientport<<endl;
if(clientfd<0){
perror("epoll_wait is error");
continue;;
}
//把这位道友放到epoll里
struct epoll_event c_epoll;
c_epoll.events = EPOLLIN;
c_epoll.data.fd=clientfd;
ret = epoll_ctl(epld,EPOLL_CTL_ADD,clientfd,&c_epoll);
if(ret<0){
perror("epoll_ctl is error");
continue;
}
client new_client;
new_client.sockfd=clientfd;
new_client.daohao="小王八不起名!";
conn_clients[clientfd]=new_client;
//通知大家一声
}else{
//有道友传音中
char buffer[1024];
int n= read(fd,buffer,1024);
if(n<0){
//道友传音失败
perror("read is error");
continue;
}else if(n==0){
//道友离开了
close(fd);
epoll_ctl(epld,EPOLL_CTL_DEL,fd,0);
conn_clients.erase(fd);
}else{
//道友传音中成功
std::string msg(buffer,n);
if(conn_clients[fd].daohao=="小王八不起名!")
{//小王八起名了
conn_clients[fd].daohao=msg;
}else{
//聊天消息
std::string name=conn_clients[fd].daohao;
cout<<msg<<endl;
//发给所有人
for(auto&c:conn_clients){
write(c.first,('['+name+']'+": "+msg).c_str(),msg.size()+name.size()+4);
}
}
}
}
}
}
//关闭epoll
for(auto&c:conn_clients){
close(c.first);
epoll_ctl(epld,EPOLL_CTL_DEL,c.first,0);
}
close(epld);
close(listenfd);
}
Windows下客户端
windows(GBK)与linux(UTF8)编码方式不同,要进行转码工作,通过Unicode进行中继替换,客户端将windows的dos界面划分为两部分,在上面printf表明是大家的聊天信息,下面print则是自己的编辑区,还要根据中英文字符实现删除功能与消息多了自动滚动功能(没linux方便感觉),以下为代码
静态导出
改成release模式,在项目属性的代码生成中改为MT静态链接,然后直接生成,在编译输出目录下找到该exe文件
头文件
#pragma once
#pragma once
#include<stdio.h>
#include<string>
#include<WinSock2.h>
#include <Ws2tcpip.h>
#include<iostream>
#include<mutex>
#include<thread>
#include<conio.h>
#pragma comment (lib,"WS2_32.lib")
#define SERVER_IP "115.236.153.174"
#define SPEECH_PORT 55183
using namespace std;
class doscommend {
public:
string GbkToUtf8(const char* src_str);
string Utf8ToGbk(const char* src_str);
void uiinit();
void gotoxy(int x, int y);
void printf_brank();
void printf_fenge();
void ui_log();
void quit();
private:
char line1[111];//分割线
char line2[111];//空白字符
};
class client {
public:
bool init(SOCKET* socket, sockaddr_in* socket_addr);
void login();
void edit_working();
void revcwork(bool* isworing);
void quit();
private:
bool isHZ(char str[], int index);
void printmsg(const string mesg);
void editprint(int col, char ch);
void editprint(int col, const char* ch);
void clcedit();
bool istoping;
mutex my_mutex;
doscommend m_dos;
SOCKET* m_socket;//套接字
sockaddr_in* m_socket_addr;//套接字地址
};
源文件
#include"client_speech.h"
string doscommend::GbkToUtf8(const char* src_str)
{
string result;
wchar_t* strSrc;
char* szRes;
int len = MultiByteToWideChar(CP_ACP, 0, src_str, -1, NULL, 0);
strSrc = new wchar_t[len + 1];
MultiByteToWideChar(CP_ACP, 0, src_str, -1, strSrc, len);
len = WideCharToMultiByte(CP_UTF8, 0, strSrc, -1, NULL, 0, NULL, NULL);
szRes = new char[len + 1];
WideCharToMultiByte(CP_UTF8, 0, strSrc, -1, szRes, len, NULL, NULL);
result = szRes;
if (strSrc)
delete[]strSrc;
if (szRes)
delete[]szRes;
return result;
}
string doscommend::Utf8ToGbk(const char* src_str)
{
string result;
wchar_t* strSrc;
char* szRes;
int len = MultiByteToWideChar(CP_UTF8, 0, src_str, -1, NULL, 0);
strSrc = new wchar_t[len + 1];
MultiByteToWideChar(CP_UTF8, 0, src_str, -1, strSrc, len);
len = WideCharToMultiByte(CP_ACP, 0, strSrc, -1, NULL, 0, NULL, NULL);
szRes = new char[len + 1];
WideCharToMultiByte(CP_ACP, 0, strSrc, -1, szRes, len, NULL, NULL);
result = szRes;
if (strSrc)
delete[]strSrc;
if (szRes)
delete[]szRes;
return result;
}
void doscommend::printf_brank() {
printf("%s\n", line2);
}
void doscommend::printf_fenge() {
printf("%s\n", line1);
}
void doscommend::ui_log() {
system("mode con lines=10 cols=30\n");
}
bool client::init(SOCKET* new_socket, sockaddr_in* new_socket_addr) {
m_socket = new_socket;
m_socket_addr = new_socket_addr;
istoping = false;
//1.网络服务初始化
WSADATA data;
int ret = WSAStartup(MAKEWORD(1, 1), &data);
if (ret != 0)
return false;
//2.网络套接字
*m_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//3.物理地址
m_socket_addr->sin_family = PF_INET;
inet_pton(AF_INET, SERVER_IP, &m_socket_addr->sin_addr);
m_socket_addr->sin_port = htons(SPEECH_PORT);
return true;
}
void client::login() {
string daohao;
m_dos.ui_log();
printf("欢迎使用修仙聊天群\n\n");
printf("道友道号为何?");
getline(cin, daohao);
m_dos.uiinit();
string temputf8 = m_dos.GbkToUtf8(daohao.c_str());//转换编码
send(*m_socket, temputf8.c_str(), temputf8.size(), 0);
}
void doscommend::gotoxy(int x, int y) {
HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(hout, pos);
}
void doscommend::uiinit() {
system("mode con lines=36 cols=110");
system("cls");
gotoxy(0, 33);//移动光标
for (int i = 0; i < 110; i++) {
line1[i] = '-';
line2[i] = ' ';
}
line1[110] = 0;
line2[110] = 0;
printf("%s\n", line1);
}
void client::printmsg(const string mesg) {
//上锁
my_mutex.lock();
static POINT pos = { 0,0 };
m_dos.gotoxy(pos.x, pos.y);
cout << mesg << endl;
HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO info;
GetConsoleScreenBufferInfo(hout, &info);
pos.x = info.dwCursorPosition.X;
pos.y = info.dwCursorPosition.Y;
if (pos.y >= 33) {
m_dos.printf_brank();
printf("\n\n");
m_dos.gotoxy(0, 33);
m_dos.printf_fenge();
pos.y -= 1;
}
m_dos.gotoxy(1, 34);
my_mutex.unlock();
}
void client::editprint(int col, char ch) {
my_mutex.lock();
m_dos.gotoxy(col, 34);
printf("%c", ch);
my_mutex.unlock();
}
void client::editprint(int col, const char* ch) {
my_mutex.lock();
m_dos.gotoxy(col, 34);
printf("%s", ch);
my_mutex.unlock();
}
void client::clcedit() {
my_mutex.lock();
m_dos.gotoxy(0, 34);
m_dos.printf_brank();
my_mutex.unlock();
}
void client::revcwork(bool* isworing) {
char buff[4096];
while (!istoping) {
int ret = recv(*m_socket, buff, sizeof(buff), 0);
if (ret <= 0) {
printf("服务器出错!\n");
break;
}
string temputf8(buff, ret);
printmsg(m_dos.Utf8ToGbk(temputf8.c_str()));
}
*isworing = false;
}
bool client::isHZ(char str[], int index) {
//一个汉字两个字节,第一个字节<0
//一个英文字符,只有一个字节,>0
//从头遍历出现一个负数就是汉字
int i = 0;
while (i < index) {
if (str[i] > 0) {
i++;
}
else {
i += 2;
}
}
if (i == index)
return false;
else
return true;
}
void client::edit_working() {
while (!istoping) {
char buff[1024];//保存字符串
memset(buff, 0, sizeof(buff));
clcedit();
editprint(0, '>');
int len = 0;
while (1) {
if (_kbhit()) {
char c = _getch();
if (c == '\r') {
//输入回车
break;
}
else if (c == 8) {
//退格键阿斯克码为8
if (len == 0)
continue;
if (isHZ(buff, len - 1)) {
editprint(len + 1, "\b\b \b\b");
buff[len - 1] = 0;
buff[len - 2] = 0;
len -= 2;
}
else {
editprint(len + 1, "\b \b");
buff[len - 1] = 0;
len -= 1;
}
continue;
}
my_mutex.lock();
do {
printf("%c", c);
buff[len++] = c;
} while (_kbhit() && (c = _getch()));
my_mutex.unlock();
}
}
if (len == 0) {
continue;
}
//判断是否为退出信息
string tempgbk(buff, len);
if (strncmp(buff, "我妈叫我回去吃饭!", 4) == 0)
istoping = true;
//发送信息
string temputf8 = m_dos.GbkToUtf8(buff);//转换编码
send(*m_socket, temputf8.c_str(), temputf8.size(), 0);
}
}
void client::quit() {
my_mutex.lock();
for (int i = 0; i < 33; i++) {
m_dos.gotoxy(0, i);
m_dos.printf_brank();
Sleep(50);
m_dos.gotoxy(0, i);
printf("%s", "正在回家吃饭!");
}
my_mutex.unlock();
}
int main(void) {
client windows_client;
SOCKET server_socket;//套接字
sockaddr_in socket_addr;//套接字地址
bool isworing = true;//运行标志
if (!windows_client.init(&server_socket, &socket_addr)) {
printf("初始化失败!\n");
return -1;
}
int ret = connect(server_socket, (SOCKADDR*)&socket_addr, sizeof(socket_addr));
if (ret != 0) {
printf("连接失败\n");
return -2;
}
//登录聊天室
windows_client.login();
//接受信息
thread m_revcmes(&client::revcwork, &windows_client, &isworing);
//编辑信息
windows_client.edit_working();
m_revcmes.join();
while (isworing) {
Sleep(2000);
}
windows_client.quit();
return 0;
}
linux下客户端
linux比较简单,也不用转码,直接收发就行,自带删除功能,就不上下分两部分了,类对讲机型聊天(笑,半双工)
想开个新窗口专门用来聊天整个启动器
编译
g++ 1.cpp -o start
g++ client_linux.cpp -o client_linux -pthread
启动器
#include<stdlib.h>
int main(){
system("gnome-terminal -e ./client_linux");
}
源代码
#include<iostream>
#include<string>
#include<string.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<map>
#include <arpa/inet.h>
#include <fcntl.h>
#include <termios.h>
#define IP "115.236.153.174"
#define PORT 55183
#define BUF_SIZE 1024
using namespace std;
bool isstoping=true;
pthread_mutex_t mutex;
void *revc_work(void *arg){
int socket_temp=*((int *)arg);
char recbuf[BUF_SIZE];
while(!isstoping){
int len=recv(socket_temp,recbuf,sizeof(recbuf)-1,0);
if(len==-1){
break;
}
recbuf[len]='\0';
pthread_mutex_lock(&mutex);
printf("%s\n",recbuf);
pthread_mutex_unlock(&mutex);
}
}
int main(){
isstoping=false;
pthread_mutex_init(&mutex,NULL);
struct termios tm, tm_old;
//创建socket
int c_socket=socket(AF_INET,SOCK_STREAM,0);
if(c_socket<0){
perror("c_socket is eror");
return -1;
}
//绑定服务器端口
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(PORT);
inet_pton(AF_INET,IP,&addr.sin_addr);
int ret =connect(c_socket,(sockaddr*)&addr,sizeof(addr));
if(ret<0){
perror("connect is error");
return -1;
}else{
cout<<"欢迎进入修仙聊天群"<<endl;
cout<<" 道友道号为何?";
}
//接收消息
pthread_t thread_recv;
if(pthread_create(&thread_recv, NULL, revc_work,&c_socket)!= 0) {
perror("thread_recv is error");
return -1;
}
//发消息
string s;
while(!isstoping){
//if(kbhit){
//pthread_mutex_lock(&mutex);
cout<<">";
getline(cin,s);
send(c_socket,s.c_str(),s.size(),0);
printf("\033[1A"); //先回到上一行
printf("\033[K"); //清除该行
if(s=="我妈叫我"){
isstoping=true;
for(int i=0;i<5;i++){
printf("\033[K"); //清除该行
printf("%s","我妈叫我回去吃饭!\n");
sleep(1);
printf("\033[1A"); //先回到上一行
printf("\033[1A"); //先回到上一行
}
}
// pthread_mutex_unlock(&mutex);
// }
}
pthread_join(thread_recv,NULL);
return 0;
}
花生内网穿透
附加穿透教程
sudo dpkg -i phddns_5_1_amd64.deb
检查
phddns
(phddns |start|status|stop|restart|reset|enable|disable|version)
然后上官网
主机就填私有ip就行,端口写服务器的端口(可用22shell的测试下)
1.
2.
然后诊断找到域名ip,记下以后客户端填这个ip地址
如果诊断不成功别慌可能是你服务器没开,在编辑里吧端口改成22,再诊断测试下