你可以做我猜的。首先创建一个用于套接字通信的类并确保它可以工作..
我有一些我不久前在下面写的工作代码。您可以在下面看到它是如何完成的。我还上传了服务器和客户端的源代码(使用 Codeblocks 和 gcc/g++ 4.8.1 编译它):http ://www.mediafire.com/download/6j84bedkp3s3sq5/套接字+聊天.zip
套接字.hpp:
#ifndef SOCKETS_HPP_INCLUDED
#define SOCKETS_HPP_INCLUDED
#include <Winsock2.h>
#include <Windows.h>
#include <Ws2tcpip.h>
#include <iostream>
#include <stdexcept>
#define WM_SOCKET 0x10000
class Socket
{
private:
SOCKET socket;
std::uint32_t Port;
std::string Address;
HWND WindowHandle;
bool Listen, Initialized, Asynchronous;
public:
Socket(){};
Socket(std::uint32_t Port, std::string Address, bool Listen = false, HWND WindowHandle = nullptr, bool Asynchronous = false);
~Socket();
int Recv(void* Buffer, std::uint32_t BufferLength);
int Recv(SOCKET S, void* Buffer, std::uint32_t BufferLength);
int Send(void* Buffer, std::size_t BufferSize);
int Send(SOCKET S, void* Buffer, std::size_t BufferSize);
void Connect(std::uint32_t Port, std::string Address, bool Listen, HWND WindowHandle, bool Asynchronous);
SOCKET Accept(sockaddr* ClientInfo, int* ClientInfoSize);
SOCKET GetSocket() const;
void Close();
};
#endif // SOCKETS_HPP_INCLUDED
套接字.cpp:
#include "Sockets.hpp"
std::string ErrorMessage(std::uint32_t Error, bool Throw = true)
{
LPTSTR lpMsgBuf = nullptr;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, Error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast<LPTSTR>(&lpMsgBuf), 0, nullptr);
if (Throw)
{
throw std::runtime_error(lpMsgBuf);
}
return lpMsgBuf;
}
Socket::~Socket()
{
Close();
}
void Socket::Close()
{
if (socket)
{
shutdown(socket, SD_BOTH);
closesocket(socket);
socket = 0;
}
if (Initialized)
{
WSACleanup();
}
}
SOCKET Socket::GetSocket() const {return this->socket;}
Socket::Socket(std::uint32_t Port, std::string Address, bool Listen, HWND WindowHandle, bool Asynchronous) : socket(0)
{
Connect(Port, Address, Listen, WindowHandle, Asynchronous);
}
void Socket::Connect(std::uint32_t Port, std::string Address, bool Listen, HWND WindowHandle, bool Asynchronous)
{
if (!socket)
{
this->Port = Port;
this->Address = Address;
this->Listen = Listen;
this->WindowHandle = WindowHandle;
this->Asynchronous = Asynchronous;
this->Initialized = true;
WSADATA wsaData;
struct sockaddr_in* sockaddr_ipv4;
if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0)
{
throw std::runtime_error("Error: " + ErrorMessage(WSAGetLastError()));
}
if ((this->socket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET)
{
this->Close();
throw std::runtime_error("Error: " + ErrorMessage(WSAGetLastError()));
}
if (Address != "INADDR_ANY")
{
struct addrinfo *result = nullptr;
getaddrinfo(Address.c_str(), nullptr, nullptr, &result);
struct addrinfo* it;
for (it = result; it != nullptr; it = it->ai_next)
{
sockaddr_ipv4 = reinterpret_cast<sockaddr_in*>(it->ai_addr);
Address = inet_ntoa(sockaddr_ipv4->sin_addr);
if (Address != "0.0.0.0") break;
}
freeaddrinfo(result);
}
SOCKADDR_IN SockAddr;
memset(&SockAddr, 0, sizeof(SockAddr));
SockAddr.sin_port = htons(Port);
SockAddr.sin_family = AF_INET;
SockAddr.sin_addr.s_addr = (Address == "INADDR_ANY" ? htonl(INADDR_ANY) : inet_addr(Address.c_str()));
if (Listen && (bind(this->socket, reinterpret_cast<SOCKADDR*>(&SockAddr), sizeof(SockAddr)) == SOCKET_ERROR))
{
this->Close();
std::string Error = ErrorMessage(WSAGetLastError());
throw std::runtime_error("Error: " + ErrorMessage(WSAGetLastError()));
}
if (Asynchronous && WindowHandle)
{
if(WSAAsyncSelect(socket, WindowHandle, WM_SOCKET, FD_READ | FD_WRITE | FD_CONNECT | FD_CLOSE | FD_ACCEPT) != 0)
{
this->Close();
throw std::runtime_error("Error: " + ErrorMessage(WSAGetLastError()));
}
}
if (Listen && (listen(this->socket, SOMAXCONN) == SOCKET_ERROR))
{
this->Close();
throw std::runtime_error("Error: " + ErrorMessage(WSAGetLastError()));
}
if(!Listen && (connect(this->socket, reinterpret_cast<SOCKADDR*>(&SockAddr), sizeof(SockAddr)) == SOCKET_ERROR))
{
if(Asynchronous && WindowHandle && (WSAGetLastError() != WSAEWOULDBLOCK))
{
this->Close();
throw std::runtime_error("Error: " + ErrorMessage(WSAGetLastError()));
}
}
}
}
SOCKET Socket::Accept(sockaddr* ClientInfo, int* ClientInfoSize)
{
static int Size = sizeof(sockaddr);
return accept(this->socket, ClientInfo, (ClientInfo && ClientInfoSize ? ClientInfoSize : &Size));
}
int Socket::Recv(void* Buffer, std::uint32_t BufferLength)
{
return recv(this->socket, reinterpret_cast<char*>(Buffer), BufferLength, 0);
}
int Socket::Recv(SOCKET S, void* Buffer, std::uint32_t BufferLength)
{
return recv(S, reinterpret_cast<char*>(Buffer), BufferLength, 0);
}
int Socket::Send(void* Buffer, std::size_t BufferSize)
{
return send(this->socket, reinterpret_cast<char*>(Buffer), BufferSize, 0);
}
int Socket::Send(SOCKET S, void* Buffer, std::size_t BufferSize)
{
return send(S, reinterpret_cast<char*>(Buffer), BufferSize, 0);
}
以上只是对套接字的一个封装,它使事情变得非常容易使用。
现在对于客户端和服务器窗口,我围绕 WinAPI 函数创建了另一个包装器,以使创建窗口变得容易!
窗口.hpp:
#ifndef WINDOW_HPP_INCLUDED
#define WINDOW_HPP_INCLUDED
#include <windows.h>
#include <string>
class Window
{
private:
HWND WindowHandle;
static LRESULT __stdcall WindowProcedure(HWND Hwnd, UINT Msg, WPARAM wParam, LPARAM lParam);
public:
void Create(std::string ClassName, std::string Title, int Width = CW_USEDEFAULT, int Height = CW_USEDEFAULT, WNDPROC WindowProcedure = nullptr, WNDCLASSEX WndClass = {0});
HWND GetWindowHandle();
};
#endif // WINDOW_HPP_INCLUDED
窗口.cpp:
#include "Window.hpp"
LRESULT __stdcall Window::WindowProcedure(HWND Hwnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
switch(Msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(Hwnd, Msg, wParam, lParam);
}
return 0;
};
void Window::Create(std::string ClassName, std::string Title, int Width, int Height, WNDPROC WindowProcedure, WNDCLASSEX WndClass)
{
if (WindowProcedure == nullptr)
{
WindowProcedure = Window::WindowProcedure;
}
if (WndClass.cbSize == 0)
{
WndClass =
{
sizeof(WNDCLASSEX), CS_DBLCLKS, WindowProcedure,
0, 0, GetModuleHandle(nullptr), LoadIcon(nullptr, IDI_APPLICATION),
LoadCursor(nullptr, IDC_ARROW), HBRUSH(COLOR_WINDOW),
nullptr, ClassName.c_str(), LoadIcon (nullptr, IDI_APPLICATION)
};
}
if(RegisterClassEx(&WndClass))
{
this->WindowHandle = CreateWindowEx(0, ClassName.c_str(), Title.c_str(), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, Width, Height, nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
if(WindowHandle)
{
MSG msg = {nullptr};
ShowWindow(WindowHandle, SW_SHOWDEFAULT);
while(GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
}
协议.hpp:
#include "Sockets.hpp"
#include <iostream>
/**
Packet Protocol Definition. Can probably make this into an Enum later when it gets more complex.
This protocol determines how a packet is read. Is it a packet for the server? The client? What type? etc..
**/
const int PACKET_PROTOCOL_SERVER_ID = -3; //A Server packet telling all clients a global message or Sends clients a unique ID upon connect.
const int PACKET_PROTOCOL_UPDATE_ID = -2; //Sends a packet to the server telling it we want to update other clients with our new info.
const int PACKET_PROTOCOL_CLIENT_DISC = -1; //A client has disconnected, update our contacts list.
const int PACKET_PROTOCOL_CLIENT_CONN = 0; //A client has connected, update our contacts list.
//const int PACKET_PROTOCOL_ //Add other protocols such as admin-login, authenticate, etc..
//If you add more, don't forget to update the Packet struct.
/**
A structure that represents a packet to be sent over a network/socket.
**/
struct Packet
{
std::int32_t ID;
std::int32_t To;
std::int32_t From;
std::string Name;
std::string Message;
};
template <typename T>
T ReadPointer(char*& Pointer)
{
T Result = *(reinterpret_cast<T*>(Pointer));
Pointer += sizeof(T);
return Result;
}
template <typename T>
void WritePointer(char*& Pointer, const T& Value)
{
*(reinterpret_cast<T*>(Pointer)) = Value;
Pointer += sizeof(T);
}
/**
Serializes a packet into a buffer of unsigned-chars.. aka bytes. Then sends it through the socket.
**/
bool WritePacket(SOCKET s, Packet &packet)
{
if (s)
{
std::vector<char> Buffer((sizeof(int32_t) * 3) + sizeof(packet.Name.size()) + packet.Name.size() + sizeof(packet.Message.size()) + packet.Message.size(), 0);
char* Ptr = Buffer.data();
WritePointer(Ptr, packet.ID);
WritePointer(Ptr, packet.To);
WritePointer(Ptr, packet.From);
WritePointer(Ptr, packet.Name.size());
for (auto it = packet.Name.begin(); it != packet.Name.end(); ++it)
WritePointer(Ptr, *it);
WritePointer(Ptr, packet.Message.size());
for (auto it = packet.Message.begin(); it != packet.Message.end(); ++it)
WritePointer(Ptr, *it);
send(s, Buffer.data(), Buffer.size(), 0);
return true;
}
return false;
}
/**
Deserializes a buffer of unsigned-chars.. aka bytes back into a packet.
**/
bool ReadPacket(SOCKET s, Packet &packet)
{
if (s)
{
packet.Name.clear();
recv(s, reinterpret_cast<char*>(&packet.ID), sizeof(packet.ID), 0);
recv(s, reinterpret_cast<char*>(&packet.To), sizeof(packet.To), 0);
recv(s, reinterpret_cast<char*>(&packet.From), sizeof(packet.From), 0);
decltype(packet.Name.size()) Size = 0;
recv(s, reinterpret_cast<char*>(&Size), sizeof(Size), 0);
std::vector<char> Buffer(Size, 0);
recv(s, Buffer.data(), Buffer.size(), 0);
packet.Name.append(Buffer.begin(), Buffer.end());
Buffer.clear();
Size = 0;
recv(s, reinterpret_cast<char*>(&Size), sizeof(Size), 0);
recv(s, Buffer.data(), Buffer.size(), 0);
packet.Message.append(Buffer.begin(), Buffer.end());
return true;
}
return false;
}
上述所有头文件和源文件都将在客户端和服务器中使用。协议是客户端和服务器之间的链接。它描述了数据包以及如何读/写它。这是服务器和客户端来回通信的方式!
对于我做的服务器:
启动
时:如上所示,服务器必须首先运行。它将在 localhost 上侦听,并将在端口 27015 上侦听。
在客户端连接上:
客户端连接后,客户端将获得一个唯一 ID 并添加到服务器上的列表中。您可以在 FD_ACCEPT 中看到这种情况。
On Message Received:
接下来,如果客户端发送消息,则触发FD_READ,服务器开始读取客户端发送的数据包。如果是更新数据包,则该数据包将发送到服务器上的所有其他客户端,以便其他客户端可以更新有关发送数据包的客户端的信息。我们还更新存储在服务器上的客户端信息。如果它是一个服务器数据包,我们使用它并将其发送回客户端。
客户端断开连接:
断开连接时,即 FD_CLOSE,您可以看到服务器向所有其他客户端发送断开连接数据包,通知他们“某些客户端”已断开连接。然后它从列表中删除客户端并关闭客户端的套接字。
服务器的main.cpp:
#include "Sockets.hpp"
#include "Window.hpp"
#include "Protocol.hpp"
#include <vector>
#include <map>
/**
Packet Protocol Definition. Can probably make this into an Enum later when it gets more complex.
This protocol determines how a packet is read. Is it a packet for the server? The client? What type? etc..
**/
Socket* sock = nullptr;
bool SocketConnected = false;
/**
Stores information about each client that connects.
**/
std::vector<std::tuple<int, SOCKET, Packet>> Clients;
auto FindClient(int ID) -> decltype(Clients.begin())
{
for (auto it = Clients.begin(); it != Clients.end(); ++it)
{
if (std::get<0>(*it) == ID)
return it;
}
return Clients.end();
}
auto FindClient(SOCKET socket) -> decltype(Clients.begin())
{
for (auto it = Clients.begin(); it != Clients.end(); ++it)
{
if (std::get<1>(*it) == socket)
return it;
}
return Clients.end();
}
void SendAll(Packet &packet)
{
for (auto it = Clients.begin(); it != Clients.end(); ++it)
{
if (std::get<0>(*it) != packet.From)
{
packet.To = std::get<0>(*it);
WritePacket(std::get<1>(*it), packet);
}
}
}
LRESULT __stdcall WindowProcedure(HWND Hwnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
switch(Msg)
{
case WM_CREATE:
{
sock = new Socket(27015, "INADDR_ANY", true, Hwnd, true);
}
break;
case WM_SOCKET: /** We received a socket event **/
{
switch(WSAGETSELECTEVENT(lParam))
{
case FD_WRITE:
{
SocketConnected = true;
}
break;
case FD_READ: /** We have received a packet from the client. Read the ID and interpret the packet information. **/
{
Packet P;
ReadPacket(reinterpret_cast<SOCKET>(wParam), P);
if (P.ID == PACKET_PROTOCOL_UPDATE_ID)
{
auto it = FindClient(P.From);
if (it != Clients.end())
{
Packet* Client = &std::get<2>(*it);
Client->Name = P.Name;
for (auto it = Clients.begin(); it != Clients.end(); ++it)
{
P.ID = PACKET_PROTOCOL_UPDATE_ID;
P.From = std::get<0>(*it);
SendAll(P);
}
}
}
else if (P.ID == PACKET_PROTOCOL_SERVER_ID)
{
auto it = FindClient(P.To);
if (it != Clients.end())
{
WritePacket(std::get<1>(*it), P);
}
}
SocketConnected = true;
}
break;
case FD_ACCEPT: //A client wants to connect. We accept them and store them in our list.
{
int ClientID = 1;
while(FindClient(ClientID) != Clients.end())
{
++ClientID;
}
Packet Client;
sockaddr_in ClientAddressInfo = {0};
Clients.push_back(std::make_tuple(ClientID, sock->Accept(reinterpret_cast<sockaddr*>(&ClientAddressInfo), nullptr), Client));
Packet PacketInfo;
PacketInfo.ID = PACKET_PROTOCOL_SERVER_ID;
PacketInfo.To = ClientID;
SocketConnected = true;
WritePacket(std::get<1>(Clients.back()), PacketInfo);
}
break;
case FD_CLOSE: //A client has disconnected. Notify all other clients and remove the client from our list.
{
auto it = FindClient(reinterpret_cast<SOCKET>(wParam));
if (it != Clients.end())
{
Packet PacketInfo;
PacketInfo.ID = PACKET_PROTOCOL_CLIENT_DISC;
PacketInfo.From = std::get<0>(*it);
SendAll(PacketInfo);
Clients.erase(it);
}
}
break;
default:
break;
}
break;
}
case WM_DESTROY:
{
sock->Close();
delete sock;
WSACleanup();
PostQuitMessage(0);
}
return 0;
default:
return DefWindowProc(Hwnd, Msg, wParam, lParam);
}
return 0;
}
int main()
{
Window().Create("Server", "Server", 200, 100, WindowProcedure, {0});
}
对于我做的客户:
On Start:
创建窗口时,它会尝试连接到服务器。
On Connect:
连接到服务器后,收到 FD_WRITE 消息。在 FD_WRITE 开关案例中,我们简单地构建一个基本数据包并将其发送到服务器,让他们知道我们的姓名、我们的信息等。服务器将接收一个 UPDATE 数据包并更新我们的信息,并通知所有其他客户端我们存在。
On Receive:
收到消息后,触发 FD_READ。我们需要读取从服务器收到的数据包,并按照我们的意愿解释它。目前,从服务器收到的第一个数据包是我们发给我们的唯一 ID 以及所有已连接联系人的列表。
此外,如果数据包不是服务器数据包,我们只需读取数据包并将文本附加到接收框!这将显示服务器上其他联系人或客户端收到的所有消息。
断开连接:
收到断开连接消息(FD_CLOSE)后,我决定关闭套接字而不是重试连接。它是如此简单。如果您愿意,您可以delete sock; sock = new....
重新连接。
客户端的 main.cpp:
#include "Sockets.hpp"
#include "Window.hpp"
#include "Protocol.hpp"
#include <vector>
/**
Global variables:
Socket
ClientID
Handles for controls
IDs for controls
**/
int ClientID = -1;
int ReceiverID = -1;
std::string ClientName = "Client";
Socket* sock = nullptr;
bool SocketConnected = false;
HWND SendBox, ReceiveBox, SendButton;
enum {SENDBOX_ID, RECEIVEBOX_ID, SENDBUTTON_ID};
LRESULT __stdcall WindowProcedure(HWND Hwnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
switch(Msg)
{
case WM_CREATE:
{
ReceiveBox = CreateWindowEx(WS_EX_STATICEDGE, "Edit", nullptr, WS_CHILD | WS_VISIBLE | ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY, 10, 10, 465, 275, Hwnd, (HMENU)RECEIVEBOX_ID, nullptr, nullptr);
SendBox = CreateWindowEx(WS_EX_STATICEDGE, "Edit", nullptr, WS_CHILD | WS_VISIBLE | ES_MULTILINE | ES_AUTOVSCROLL, 10, 315, 465, 110, Hwnd, (HMENU)SENDBOX_ID, nullptr, nullptr);
SendButton = CreateWindowEx(0, "Button", "Send", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 385, 430, 90, 25, Hwnd, (HMENU)SENDBUTTON_ID, nullptr, nullptr);
sock = new Socket(27015, "localhost", false, Hwnd, true);
}
break;
case WM_COMMAND: /** We received an event from a button/control **/
{
switch(LOWORD(wParam))
{
case SENDBUTTON_ID: //The send button was pressed so we want to construct a packet from the sendbox's contents and send it to the server.
{
Packet P;
std::vector<std::uint8_t> Buffer(GetWindowTextLength(SendBox) + 1);
GetWindowText(SendBox, reinterpret_cast<char*>(Buffer.data()), Buffer.size());
P.Message.append(Buffer.begin(), Buffer.end());
if (!P.Message.empty())
{
P.ID = ReceiverID; //The packet is NOT meant for the server. It is meant for the client.
P.To = ReceiverID; //We will be sending the packet to some other client.
P.From = ClientID; //The packet is from this client.
P.Name = ClientName; //Our name..
WritePacket(sock->GetSocket(), P);
}
}
break;
case RECEIVEBOX_ID:
{
if (HIWORD(wParam) == EN_SETFOCUS)
{
HideCaret(ReceiveBox);
}
else if (HIWORD(wParam) == EN_KILLFOCUS)
{
ShowCaret(ReceiveBox);
}
}
break;
}
}
break;
case WM_SOCKET: /** We received a socket event **/
{
switch(WSAGETSELECTEVENT(lParam))
{
case FD_WRITE: /** We connected to the server successfully so we need to send an initialization packet. **/
{
SocketConnected = true;
Packet P;
P.ID = PACKET_PROTOCOL_UPDATE_ID;
P.To = PACKET_PROTOCOL_SERVER_ID;
P.From = -1; /** The server will send us a unique Identifier. **/
P.Name = "ICantChooseUsernames";
P.Message = "Hello";
WritePacket(sock->GetSocket(), P);
}
break;
case FD_READ: /** We have received a packet from the server. Read the ID and interpret the packet information. **/
{
Packet P;
ReadPacket(sock->GetSocket(), P);
if (P.ID == PACKET_PROTOCOL_SERVER_ID) //If the packet is a server packet, then it is sending us our Unique client ID.
{
ClientID = P.To;
SetWindowText(Hwnd, (ClientName + ": " + std::to_string(P.ID)).c_str()); //Set the window title to "OurName: " + OurID.
}
else if (P.ID == PACKET_PROTOCOL_CLIENT_DISC)
{
//Delete the specified contact from our contacts list..
}
else if (P.ID == PACKET_PROTOCOL_CLIENT_CONN)
{
//Add the client to the contacts list..
}
else //Else print the packet's message in the received box..
{
std::string Sender = P.Name;
std::string Message = P.Message;
int ReceiveBoxLength = GetWindowTextLength(ReceiveBox);
Message = ReceiveBoxLength == 0 ? Sender + ": " + Message : "\r\n\r\n" + Sender + ": " + Message;
SendMessage(ReceiveBox, EM_SETSEL, -1, -1);
SendMessage(ReceiveBox, EM_REPLACESEL, 0, reinterpret_cast<LPARAM>(Message.c_str()));
}
}
break;
case FD_CLOSE:
{
sock->Close();
}
break;
default:
break;
}
break;
}
case WM_DESTROY:
{
sock->Close();
delete sock;
WSACleanup();
PostQuitMessage(0);
}
return 0;
default:
return DefWindowProc(Hwnd, Msg, wParam, lParam);
}
return 0;
}
int main()
{
Window().Create("Client", "Client", 500, 500, WindowProcedure, {0});
}