使用socket做了一个类似群聊功能的demo,来记录这两天学习socket的成果。
在实现功能之前先看下sockect通信的基本流程:
socket服务端:
1.socket():创建套接字
2.bind():将创建的套接字绑定到一个本地地址和端口上
3.listen():监听套接字,准备接受客户请求
4.accept():接收客户端请求,返回一个对应此连接新套接字
5.用accept()返回的套接字和客户端进行通信,recv()/send() 接受/发送信息
6.返回,等待另一个客户请求
7.关闭套接字
socket客户端:
1.socket():创建套接字
2.connect():建立到达服务器的连接
3.与服务器进行通信,recv()/send()接受/发送信息
5.close():关闭客户连接
服务器的关键实现有两点:
1.群聊即一个客户端发送的消息,服务器广播给该群内在线的所有客户端,那么服务器需要用一个容器记录群内客户端的在线情况:链接上来的客户端放入容器,断开链接从容器中移除;
2.方便管理每收到一个链接创建一个线程来管理对应的通信;
实现代码如下:
#include <iostream>
#include <strstream>
#include <set>
#include <string>
#include <thread>
#include "winsock.h"
#pragma comment(lib, "WS2_32") // 链接到WS2_32.lib
using namespace std;
std::set<SOCKET> m_socketSet;
std::string intToString(int v){
std::strstream ss;
ss << v;
std::string str;
ss >> str;
return str;
}
int main()
{
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
if (::WSAStartup(sockVersion, &wsaData) != 0)
{
exit(0);
}
SOCKET s_handle = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s_handle == INVALID_SOCKET)
{
printf("failed socket()");
return 0;
}
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567); //1024 ~ 49151:普通用户注册的端口号
sin.sin_addr.S_un.S_addr = INADDR_ANY;
if (::bind(s_handle, (sockaddr *)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf("failed bind()");
return 0;
}
if (::listen(s_handle, 10) == SOCKET_ERROR)
{
printf("failed listen()");
return 0;
}
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
char szText[] = "hello client...\n";
void handleClient(SOCKET sClient);
while (true)
{
SOCKET sClient = ::accept(s_handle, (SOCKADDR*)&remoteAddr, &nAddrLen);
if (sClient == INVALID_SOCKET)
{
printf("Failed accept()");
}
else
{
m_socketSet.insert(sClient);
//记录链接上来的客户端
printf("connect sucess:%s:%s \n", inet_ntoa(remoteAddr.sin_addr),intToString(sClient).c_str());
::send(sClient, szText, strlen(szText), 0);
//通知客户端链接成功
auto pt = std::thread(std::bind(handleClient, sClient));
pt.detach();
//创建子线程 处理对应客户端的逻辑
}
}
closesocket(s_handle);
//关闭服务器套接字
getchar();
return 0;
}
void handleClient(SOCKET sClient)
{
std::string msg = "hello " + intToString(sClient);
while (true)
{
char buff[256] = { 0 };
int nRecv = ::recv(sClient, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
printf("recv data:%s\n", buff);
int code = strcmp(buff, "hello everyone ...\0");
//判断是否广播
if (code == 0)
{
std::string notice = "hi everybody i am " + intToString(sClient);
for (auto handle:m_socketSet)
{
//便利在线的客户端广播
::send(handle, notice.c_str(),notice.size(), 0);
}
}
else{
//与对应客户端通讯
::send(sClient, msg.c_str(), msg.size(), 0);
}
}
else{
break;
}
}
m_socketSet.erase(sClient);
//链接断开 从容器中删除
closesocket(sClient);
//关闭套接字
}
客户端部分主要是通络通信线程与主线程分离,实现代码如下:
// SocketClient.cpp : 定义控制台应用程序的入口点。
//
#include <stdio.h>
#include <tchar.h>
#include "wrapper.h"
int _tmain(int argc, _TCHAR* argv[])
{
wrapper::getInstance()->createSocket();
char buff[1024] = { 0 };
while (true)
{
if (wrapper::getInstance()->getServerRespond())
{
printf("please input msg:\n");
gets(buff);
wrapper::getInstance()->sendMsg(buff, strlen(buff));
}
}
getchar();
return 0;
}
客户端封装的网络通信类:
wrapper.h
#ifndef __wrapper__
#define __wrapper__
#include <thread>
#include "winsock.h"
class wrapper
{
public:
wrapper();
~wrapper();
static wrapper *getInstance();
void createSocket();
bool sendMsg(const char *msg,int len);
bool getServerRespond();
protected:
void init();
void listenCheck();
private:
SOCKET m_sockId;
std::thread m_thread;
bool m_serverRespond;
};
#endif // __wrapper_SCENE_H__
wrapper.cpp
#include "wrapper.h"
#pragma comment(lib, "WS2_32") // 链接到WS2_32.lib
static wrapper *instance=nullptr;
wrapper *wrapper::getInstance()
{
if (!instance)
{
instance = new wrapper;
}
return instance;
}
wrapper::wrapper()
:m_sockId(INVALID_SOCKET)
, m_serverRespond(false)
{
this->init();
}
wrapper::~wrapper()
{
}
void wrapper::init()
{
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
if (::WSAStartup(sockVersion, &wsaData) != 0)
{
printf("init socket failed \n");
}
}
void wrapper::createSocket()
{
if (m_sockId != INVALID_SOCKET){
return;
}
m_sockId = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (m_sockId == INVALID_SOCKET)
{
printf("failed socket() \n");
return;
}
// 也可以在这里调用bind函数绑定一个本地地址
// 否则系统将会自动安排
// 填写远程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(4567);
// 注意,这里要填写服务器程序(TCPServer程序)所在机器的IP地址
// 如果你的计算机没有联网,直接使用127.0.0.1即可
servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (::connect(m_sockId, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
printf("failed connect() \n");
return;
}
m_thread = std::thread(std::bind(&wrapper::listenCheck,this));
m_thread.detach();
//创建网络线程 并与主线程分离
}
void wrapper::listenCheck()
{
char buff[256];
while (TRUE)
{
//从服务器端接收数据
int nRecv = ::recv(m_sockId, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
printf("%s\n", buff);
m_serverRespond = true;
}
else{
break;
}
}
// 关闭套节字
::closesocket(m_sockId);
m_sockId = INVALID_SOCKET;
}
bool wrapper::sendMsg(const char *msg, int len)
{
if (m_sockId != INVALID_SOCKET)
{
// send msg to server
m_serverRespond = false;
int size= ::send(m_sockId, msg, len, 0);
return size;
}
else{
this->createSocket();
printf("reconnect server... \n");
}
return false;
}
bool wrapper::getServerRespond()
{
return m_serverRespond;
}
这样一个简单的类群聊功能就实现了,打开两个客户端,一个客户端输入hello everyone ... 并回车,另一个客户端就收到了消息。