[多線程通信程序]C++基於Socket的一款多人在線通信程序


廢話不多說,先上圖。

進入正題:最近閑着無聊,想起來在初二時用VB寫的一個局域網多人聊天室。當時用的是Winsock,然后寫出來給同學上信息課用,其實也沒啥用啊

今天下午突發奇想,打算用C++實現這一功能。去百度了一下相關資料,才發現C++的socket是真的麻煩。。。。。。(或許是我太菜了)於是於是於是,我很認真地開始學習(Copy)。找了好久都沒找到符合我意思的模板,而且突然發現如果要多人同時在線聊天的話,好像還要多線程來着,,ԾㅂԾ,,。於是我開始很認真的學習(別說了,是真的開始學習)。具體的socket原理請點我

經過了一個下午的學習(Copy),我終於大致清楚了socket的工作原理,以及整個通信程序的大致思路。(引用一下某大佬博客的原文ε=ε=ε=┏(゜ロ゜;)┛)

我們編程應首先確定你需要的功能即你需要實現的“方法”,如socket套接字中對端口號的設置,對IP地址的設置,對阻塞模式的設置,對其他客戶端的監聽和接收,以及各種錯誤信息的設置和反饋。而在網絡編程多人聊天室的編碼過程中,我經過多次的嘗試和總結以后,理出了一個大概的思路——即首先封裝socket套接字,然后按邏輯封裝其他的功能類。

不過我還是搞了好久。。。。。。我還是來總結一下我遇到的問題吧。

首先是編譯的問題。雖然我之前用的都是VC,但是這次我是用Dev-c++開的工程。於是在編譯的時候遇到了幾個神奇的問題。后來瀏覽了幾個大佬的博客,發現Dev-c++要自己配置編譯參數。總結一下,如果要用Winsock.h的話,需要在鏈接器里面加入-lwsock32才能用(在工程屬性->參數->鏈接器里面)。還有一個問題,就是用thread的話,需要在編譯參數內加上-std=c++11,不然也會編譯錯誤。(用VC的請自動忽略上面這段話)

然后先放個代碼吧。。。寫的不好請見諒(●'◡'●)

服務端頭文件(Server.h)

#define _WINSOCK_DEPRECATED_NO_WARNINGS 
#define  _CRT_SECURE_NO_WARNINGS
#ifndef _H_H_
#define _H_H_
#include <thread>
#include <iostream>
#include <string>
#include <WinSock2.h>
#include <stdlib.h>
#pragma  comment(lib,"ws2_32.lib")
using namespace std;
static struct MyStruct
{
	SOCKET sock;
	int empt;
}soc[4];
class Server
{
public:
	Server();
	~Server();
	static void Work(int id);
private:
	SOCKET s;
};
#endif

服務端(Server.cpp)

#include "Server.h"
Server::Server()
{
	WSADATA wsadata;
	WORD v = MAKEWORD(2, 2);
	if (::WSAStartup(v, &wsadata)<0) {cout<<"Failed to start server!"<<endl; system("pause"); exit(0);}
	s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	//服務器監聽的端口和自己的IP
	sin.sin_port = htons(13574);
	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	if (::bind(s,(LPSOCKADDR)&sin,sizeof(sin))==SOCKET_ERROR) return;
	if (::listen(s,5)==SOCKET_ERROR) return; cout<<"The server has been started!"<<endl;
	while (true)
	{
		int i;
		for (i=0; i<100; i++)
			if (soc[i].empt==0) {soc[i].empt = 1; break;}
		sockaddr_in re; int n = sizeof(re);
		soc[i].sock = ::accept(s,(SOCKADDR*)&re,&n);
		if (soc[i].sock==INVALID_SOCKET) return;
		cout<<"New client => "<<soc[i].sock<<endl;
		std::thread t(Work,i); t.detach();
	}
	closesocket(s);
}
Server::~Server() {::WSACleanup();}
void Server::Work(int id)
{
	char Msg[] = "Server => Welcome! Press [Esc] to edit your message, then press [Enter] to send!";
	::send(soc[id].sock,Msg,strlen(Msg),0);
	while (true)
	{
		char buff[1024];
		int nR = ::recv(soc[id].sock, buff, 1025, 0);
		if (nR>0)
		{
			buff[nR] = '\0';
			cout<<"Received :"<<buff<<" (id:"<<soc[id].sock<<")"<<endl;
			for (int i=0; i<100; i++)
				if (soc[i].empt) ::send(soc[i].sock,buff,strlen(buff),0);
		}
		else
		{
			cout<<"Client => "<<soc[id].sock<<" logout!"<<endl;
			closesocket(soc[id].sock);
			soc[id].empt = 0; return;
		}
	}
}
int main() {Server server; return 0;}

接下來是客戶端(Client.cpp)

#include <iostream>
#include <WinSock2.h>
#include <thread>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include "TextColor.h"
#define KEY_DOWN(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000) ? 1:0)
#pragma comment (lib,"ws2_32.lib")
using namespace std;
SOCKET s;
int Status=0;
char Name[100000],MyIP[100];
void GetMessege()
{
	char buff[1024];
	while (true)
	{
		int nR = recv(s,buff,1025,0); while (Status);
		if (nR>0)
		{
			buff[nR] = '\0';
			cout<<BLUE<<buff<<WHITE<<endl;
		}
		else if (nR<0) break;
	}
}
bool GetLocalIP(char* ip)
{
	WSADATA wsadata;
	int ret=WSAStartup(MAKEWORD(2,2),&wsadata);
	if (ret!=0) return false; char hostname[256];
	ret=gethostname(hostname,sizeof(hostname));
	if (ret==SOCKET_ERROR) return false;
	HOSTENT* host=gethostbyname(hostname);
	if (host==NULL) return false;
	strcpy(ip,inet_ntoa(*(in_addr*)*host->h_addr_list));
	return true;
}
int main()
{
	if (!GetLocalIP(MyIP)) {cout<<RED<<"Unknown Error!"<<endl; system("pause"); return 0;}
	WSADATA wsadata; WORD v=MAKEWORD(2, 2);
	if (::WSAStartup(v,&wsadata)<0) {cout<<RED<<"Unknown Error!"<<endl; system("pause"); return 0;}
	s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	//對應服務器的端口和服務器的IP
	sin.sin_port = htons(13574);
	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	if (::connect(s,(SOCKADDR*)&sin,sizeof(sin))<0)
	{
		cout<<RED<<"Can't connect to the Server!"<<endl;
		system("pause"); return 0;
	}
	cout<<GREEN<<"Connect to the server successfully!"<<endl;
	while (true)
	{
		cout<<WHITE<<"Please input your name:"; gets(Name);
		if (strlen(Name)>20) cout<<RED<<"Your name should be less than 20 characters."<<endl;
		else break;
	}
	system("cls");
	cout<<RED<<"             ---===Welcome===---"<<WHITE<<endl;
	thread t(GetMessege); t.detach();
	while (true)
	{
		Sleep(1000); while (!Status) if (KEY_DOWN(27)) Status^=1;
		char Msg[100000],SendMsg[1100]=""; strcat(SendMsg,Name);
		strcat(SendMsg,"("); strcat(SendMsg,MyIP); strcat(SendMsg,")");
		strcat(SendMsg," => "); setbuf(stdin,NULL);
		while (true)
		{
			cout<<WHITE<<"Input:"; gets(Msg);
			if (strlen(Msg)>1000) cout<<RED<<"Your message should be less than 1000 characters."<<endl;
			else break;
		}
		strcat(SendMsg,Msg); ::send(s,SendMsg,strlen(SendMsg),0); Status^=1;
	}
	closesocket(s); WSACleanup();
	return 0;
}

這里用到了一個設置輸出內容顏色的頭文件(TextColor.h)

#pragma once
#include <iostream>
#include <windows.h>
inline std::ostream& BLUE(std::ostream &s)
{
	HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); 
	SetConsoleTextAttribute(hStdout, FOREGROUND_BLUE|FOREGROUND_GREEN|FOREGROUND_INTENSITY);
	return s;
}
inline std::ostream& RED(std::ostream &s)
{
	HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); 
	SetConsoleTextAttribute(hStdout,FOREGROUND_RED|FOREGROUND_INTENSITY);
	return s;
}
inline std::ostream& GREEN(std::ostream &s)
{
	HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); 
	SetConsoleTextAttribute(hStdout,FOREGROUND_GREEN|FOREGROUND_INTENSITY);
	return s;
}
inline std::ostream& YELLOW(std::ostream &s)
{
	HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); 
	SetConsoleTextAttribute(hStdout,FOREGROUND_GREEN|FOREGROUND_RED|FOREGROUND_INTENSITY);
	return s;
}
inline std::ostream& WHITE(std::ostream &s)
{
	HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); 
	SetConsoleTextAttribute(hStdout,FOREGROUND_RED|FOREGROUND_GREEN|FOREGROUND_BLUE);
	return s;
}

struct color{
	color(WORD attribute):m_color(attribute){};
	WORD m_color;
};
template <class _Elem, class _Traits>
std::basic_ostream<_Elem,_Traits>& operator<<(std::basic_ostream<_Elem,_Traits>& i, color& c)
{
	HANDLE hStdout=GetStdHandle(STD_OUTPUT_HANDLE); 
	SetConsoleTextAttribute(hStdout,c.m_color);
	return i;
}

以上是完整的代碼,這里再提一下整個工程中遇到的問題。

最大的問題還是輸入消息的問題,一開始是無限循環讀入的,后來發現:如果在輸入過程中接收到服務器發來的消息,打印出來的話會在輸入的后面,格式都亂了。后來想了幾個辦法解決,一是可以在用戶按下回車后再把之前接收到的消息打印出來,二是可以設定一個特殊按鍵,按下后可以進入掛起狀態,然后用戶輸入信息(此過程中不會接受消息),在用戶按下回車后發送后,就將此過程中接收到的消息打印出來。仔細考慮了一下,覺得還是第二種方法比較好,於是就用了Esc鍵作為輸入鍵。

最后總結一下此程序還存在的BUG:

1.用戶輸入自己名稱或者消息時,我是用gets讀入的(不用我說了,gets的漏洞大家都知道吧)。這個漏洞不想改了(才不會告訴你是我懶)。
2.用戶沒有在輸入狀態時,如果隨便按鍵盤,然后按了個回車,[Esc]進入輸入狀態的時候就會把亂按的消息發出去。(這個東西怪我學識淺薄,即使清空了輸入緩沖區還是會發出去,如果讀者有什么更好的解決方法,請在評論中寫下,博主感激不盡!!!(ಥ _ ಥ))

畢竟我只是個高一的蒟蒻。。。程序寫得不咋滴,起碼也算是個入門的教程吧。。。。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM