Socket网络编程之UDP客户端

上次说了基于UDP聊天软件的服务器实现,还有一点忘记说了,那便是关闭套接字。这个功能自然是要写到析构函数里,如下:

------------------------------------------------------------

Myusock::USockServer::~USockServer()

{

std::string msg = "服务器已关闭!";

SendToAllClient(MESSAGE_SYSTEM_ERROR, msg);

closesocket(m_servSock);

WSACleanup();

}

在析构函数中,除了关闭套接字,还应该通知所有连接进来的客户端服务器已关闭,客户端可以根据此消息来做相应的操作,这个消息的类型被定义为MESSAGE_SYSTEM_ERROR。

这里,当客户端收到消息类型为MESSAGE_SYSTEM_ERROR的消息时,我们可以设置发送和发送给所有人键为不可点击状态,让连接服务器处理可点击状态,并提示“服务器已关闭”。

现在来看客户端,服务器已实现好了,客户端便相对的简单一些。客户端定义如下:

#include

#include //正则表达式类,用于验证IP地址的合法性

#include //智能指针

#include

#pragma comment(lib, "ws2_32.lib")

#pragma warning(disable:4996)

namespace Myusock {

class USockClient;//前向声明

//这个枚举类型用于是验证IP地址与端口的返回值

enum {

INVALID_ADDRESS,//无效的IP地址

VALID_PORT,//有效的端口号

VALID_ADDRESS_PORT//有效的IP地址和端口号

};

//消息标志,和服务器的是相对应的

enum MESSAGE_TYPE {

MESSAGE_MIN,//消息最小边界

MESSAGE_LOGIN,//登录消息

MESSAGE_LOGOUT,//退出消息

MESSAGE_CHAT_FROM_ME,//我发送指定用户的消息

MESSAGE_CHAT_TO_OTHER,//指定用户收我发的消息

MESSAGE_CHATALL,//发送给所有用户

MESSAGE_CHATALL_FROM_ME,//我发送给所有人

MESSAGE_CHATALL_TO_OTHER,//所有人收我的消息

MESSAGE_CHAT,//聊天消息

MESSAGE_SYSTEM_ERROR,//系统返回错误消息

MESSAGE_SYSTEM,//系统通知消息

MESSAGE_MAX//消息最大边界

};

const unsigned long MAX_BUF = 40960;

const unsigned int MSG_TYPE_LEN = 4;

//用户信息结构,客户端并不需要保存地址信息,所以只保存下自己的名称

typedef struct {

std::string userName;//用户名

}UserInfo;

//这是客户端界面的句柄与控件信息

typedef struct {

HWND hWnd;

CListBox* cListBox;

CButton* cButtonConnect;

CButton* cButtonSend;

CButton* cButtonSendToAll;

}OP_CONTROL;

class USockClient

{

public:

USockClient();

~USockClient();

void Connect(OP_CONTROL opControl, const std::string address,

const unsigned short port, const std::string name);//连接服务器

void SendUserInfo();//发送个人信息

void ConnectedSocket() const;//已连接Socket

void SendToOther(const std::string userName, std::string msg);//向目标用户发送消息

void SendToAll(std::string msg);//发送给所有用户

void RecvFromServer();//接收消息

private:

int IsValidAddressAndPort(const std::string address,//判断IP地址和端口号是否有效

const unsigned short port) const;

void init(const std::string address, const unsigned short port);//初始化套接字相关信息

std::string DealMessage(const char* msg, int& type) const;//处理消息结构

void FormatMsg(MESSAGE_TYPE msgType, std::string& msg);//格式化使消息为带消息类型的消息

void SetDlgNewMessage(std::string msg);

void SendToServer(const std::string msg) const;//发送消息

private:

WSADATA m_wsaData;

SOCKET m_sock;

SOCKADDR_IN m_servAddr;

std::string m_userName;

CString m_cstrMsg;

std::vector > m_vAllUserInfo;//所有用户信息

OP_CONTROL m_opControl;//指向要操作的控件

};

}

客户端的很多操作其实和服务器是一样的,只是服务器多了个"分配功能"。

我们先从验证IP地址和端口地址来说,它的函数如下:

-----------------------------------------------

int Myusock::USockClient::IsValidAddressAndPort(const std::string address, const unsigned short port) const

{

std::regex reg("^(1\\d|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."

"(1\\d|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."

"(1\\d|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."

"(1\\d|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$");

if (!std::regex_match(address, reg))

return INVALID_ADDRESS;

if (port >= 1024 && port

return VALID_PORT;

return VALID_ADDRESS_PORT;

}

在验证IP地址时使用了C++的正则表达式,因为使用别的方法我们很难来验证IP地址。std::regex是C++的正则类,里面提供了一些正则操作,它的头文件是。^表示正则表达式的开头,$表示正则表达式的结尾,中间是4段匹配IP地址的正则规则,根据这个规则,利用std::regex_macth来和传进来的address进行匹配,若是匹配失败,我们返回定义的枚举INVALID_ADDRESS,表式无效的IP地址。

关于正则的详细操作后面会总结一篇文章,这里便不详细解释了。

下面是端口的匹配,因为它的规则并不复杂,则无需使用正则了。

接下来来看初始操作,也很简单。

------------------------------------

void Myusock::USockClient::init(const std::string address, const unsigned short port)

{

int iRet = IsValidAddressAndPort(address, port);

if (INVALID_ADDRESS == iRet)

throw "IP地址错误!";

else if (iRet != VALID_PORT)

throw "端口错误";

if (WSAStartup(MAKEWORD(2, 2), &m_wsaData) != 0)

throw "WSAStartup() error!";

m_sock = socket(PF_INET, SOCK_DGRAM, 0);

if (INVALID_SOCKET == m_sock)

throw "socket() error!";

//设置为非阻塞状态

/*u_long nonblocking = 1;

ioctlsocket(m_sock, FIONBIO, &nonblocking);*/

memset(&m_servAddr, 0, sizeof(m_servAddr));

m_servAddr.sin_family = AF_INET;

m_servAddr.sin_addr.S_un.S_addr = inet_addr(address.c_str());

m_servAddr.sin_port = htons(port);

std::string servInfo = "Server IP:" + address + " Port:";

char tmp[6] = { 0 };

sprintf(tmp, "%d", port);

servInfo += tmp;

SetDlgItemText(m_opControl.hWnd, IDC_CHAT_CONTENT, CA2W(servInfo.c_str()));

}

可以设置一个已连接套接字函数,供用户自由调用,关于这个上篇有说:

void Myusock::USockClient::ConnectedSocket() const

{

//已连接的UDP socket

connect(m_sock, (SOCKADDR*)&m_servAddr, sizeof(m_servAddr));

}

现在,便可以实现Connect函数,说是Connect函数,其实并不是连接服务器,只是调用了init函数初始化了自己的信息:

---------------------------------------------------------------------------------

void Myusock::USockClient::Connect(OP_CONTROL opControl, const std::string address,

const unsigned short port, const std::string name)

{

if (name.empty())

throw "用户名不能为空!";

//这些是控件

m_userName = name;

m_opControl.hWnd = opControl.hWnd;

m_opControl.cListBox = opControl.cListBox;

m_opControl.cButtonConnect = opControl.cButtonConnect;

m_opControl.cButtonSend = opControl.cButtonSend;

m_opControl.cButtonSendToAll = opControl.cButtonSendToAll;

init(address, port);

}

那么,客户端是如何向服务器注册信息的呢?这就是SendUserInfo所做的事:

-----------------------------------------------------------------------

void Myusock::USockClient::SendUserInfo()

{

std::string userName = m_userName;

FormatMsg(MESSAGE_LOGIN, userName);

//std::cout

SendToServer(userName);//将用户名发送到服务器

}

其中,FormatMsg和服务器的版本实现是一样的,只是方便将消息格式化为带消息类型的消息。我们在刚初始化完客户端消息后,第一次的发的消息便是用户的个人登录信息MESSAGE_LOGIN,这样,就把用户添加到服务器列表中了。可以对比着看服务器的MESSAGE_LOGIN是如何处理的。

SendToServer()的实现很简单,就是封装了下sendto,使只需传入个string便可发送,方便我们调用:

-----------------------------------------------

void Myusock::USockClient::SendToServer(const std::string msg) const

{

sendto(m_sock, msg.c_str(), msg.size(), 0, (SOCKADDR*)&m_servAddr, sizeof(m_servAddr));

}

现在来看SendToOther()函数,这个函数是向指定用户发送消息,即私聊:

-------------------------------------------------------

void Myusock::USockClient::SendToOther(const std::string userName, std::string msg)

{

std::string sendMsg = m_userName + "_" + userName + "_" + msg;//发送人_收信人_消息体

FormatMsg(MESSAGE_CHAT, sendMsg);//消息类型:发送人_收信人_消息体

SendToServer(sendMsg);//将组装好消息发送给服务器

}

实现起来也是如此的简单,只要把消息重组为:消息类型:发送人_收信人_消息体这样的格式就好了。在服务器的MESSAGE_CHAT消息便是用来解析这种消息类型并实现分发的,具体也可再去看看服务器的实现。

再来看群聊消息:

-----------------------------------------

void Myusock::USockClient::SendToAll(std::string msg)

{

if (m_vAllUserInfo.empty())

throw "当前用户列表为空";

std::string sendMsg = m_userName + "_" + msg;//发送人_消息

FormatMsg(MESSAGE_CHATALL, sendMsg);//消息类型:发送人_消息

SendToServer(sendMsg);

}

还是依旧的简单,这都因为有了之前的消息类型逻辑处理,使我们发送消息只需组装为指定类型便可轻松搞定。这里,发送给所有人的格式定义为:消息类型:发送人_消息体。因为是向所有在线的用户发,便无需发送接收人了。服务器对应的处理消息类型为MESSAGE_CHATALL,也可对应服务器来看服务器是如何解析的。

客户端最重要的也是接收函数,因为上面这些操作的简单都是建立在接收函数稍微复杂的处理上的。

------------------------------------------------------

void Myusock::USockClient::RecvFromServer()

{

char recvMsg[MAXBYTE] = { 0 };

int servAddrSize = sizeof(m_servAddr);

recvfrom(m_sock, recvMsg, MAXBYTE, 0, (SOCKADDR*)&m_servAddr, &servAddrSize);

int msg_type;

//若接收的消息为空,则表示和服务器的地址和开放的端口不一致,或是服务器未开启,便设置相应键的状态,并抛出错误提示。

if (recvMsg[0] == '\0')

{

m_opControl.cButtonConnect->EnableWindow(TRUE);

m_opControl.cButtonSend->EnableWindow(FALSE);

m_opControl.cButtonSendToAll->EnableWindow(FALSE);

throw "与服务器建立连接失败,请检查端口或IP地址是否正确,或者确认是否打开了服务器!";

}

//DealMessage函数和服务器的处理是一样的,返回解析的消息体,消息类型通过引用保存在msg_type中

std::string check_msg = DealMessage(recvMsg, msg_type);//msg_type:消息类型 check_msg:消息体

if (!check_msg.compare("error"))

{

throw "不正确的消息类型.";

}

switch (msg_type)

{

//登录消息,为何客户端也需要处理这个消息呢?

//这便是用户信息同步的功能实现。服务器在收到了SendUserInfo的登录消息后,会有个同步消息,将自己的信息同步给其它用户,同时将其他人的信息同步给自己。同步给自己的信息,即其他用户的信息,服务器就是通过MESSAGE_LOGIN消息类型发送给客户端的。

case MESSAGE_LOGIN:

{

std::shared_ptr newUserInfo = std::make_shared();

newUserInfo->userName = check_msg;

m_vAllUserInfo.push_back(newUserInfo);//加入新用户的信息

//std::cout userName

m_opControl.cListBox->AddString(CA2W(newUserInfo->userName.c_str()));//将新用户加入到CList框中

std::string new_user = newUserInfo->userName + "加入了聊天室.";

//SetDlgNewMessage函数也和服务器的一样,用于获取聊天框中的消息,并将新消息加入,存于成员函数m_cstrMsg中,即此函数影响m_cstrMsg中的值。

SetDlgNewMessage(new_user);//设置m_cstrMsg中为新消息

SetDlgItemText(m_opControl.hWnd, IDC_CHAT_CONTENT, m_cstrMsg);

break;

}

//当有用户退出时,服务器继续同步用户信息,并将用户退出的信息通过MESSAGE_LOGOUT消息类型发送给客户端。这样,客户端便可以随时知道用户是否在线并设置用户列表框中的数据。

case MESSAGE_LOGOUT:

{

auto pos = std::find_if(m_vAllUserInfo.begin(), m_vAllUserInfo.end(),

[&](std::shared_ptr user) {

return user->userName == check_msg;

});

m_vAllUserInfo.erase(pos);//删除此用户信息

//std::cout

int index = m_opControl.cListBox->FindString(-1, CA2W(check_msg.c_str()));

if(index != -1)

m_opControl.cListBox->DeleteString(index);//CList列表中删除此用户

std::string exit_user = check_msg + "退出了聊天室.";

SetDlgNewMessage(exit_user);//设置m_cstrMsg中为新消息

SetDlgItemText(m_opControl.hWnd, IDC_CHAT_CONTENT, m_cstrMsg);

break;

}

//这两个同时处理,因为前面的解析操作都是一样的,避免写两次。再后面再用if判断就好了。

//这是我们说的私聊功能,分别是别人对我说的和我对其他人说的。根据类型的不同设置聊天框中显示的不同。

case MESSAGE_CHAT_FROM_ME:

case MESSAGE_CHAT_TO_OTHER:

{

int pos = check_msg.find('_');//名称标记位

if (pos == std::string::npos)

break;

std::string sendName = check_msg.substr(0, pos);//发送人名称

check_msg = check_msg.substr(pos + 1);

pos = check_msg.find('_');

if (pos == std::string::npos)

break;

std::string recvName = check_msg.substr(0, pos);//接收人名称

check_msg = check_msg.substr(pos + 1);//发送内容

if (msg_type == MESSAGE_CHAT_FROM_ME)

{

check_msg = "我对" + recvName + "説:" + check_msg;

SetDlgNewMessage(check_msg);//设置m_cstrMsg中为新消息

SetDlgItemText(m_opControl.hWnd, IDC_CHAT_CONTENT, m_cstrMsg);

}

else

{

check_msg = sendName + "对我:" + check_msg;

SetDlgNewMessage(check_msg);//设置m_cstrMsg中为新消息

SetDlgItemText(m_opControl.hWnd, IDC_CHAT_CONTENT, m_cstrMsg);

}

break;

}

//这里是群聊功能的返回数据

//分别是我对其他所有人说的和其他人广播给我的,操作和上面的一样。

case MESSAGE_CHATALL_FROM_ME:

case MESSAGE_CHATALL_TO_OTHER:

{

int pos = check_msg.find('_');//名称标记位

if (pos == std::string::npos)

break;

std::string sendName = check_msg.substr(0, pos);//发送人名称

check_msg = check_msg.substr(pos + 1);//发送内容

if (msg_type == MESSAGE_CHATALL_FROM_ME)

{

check_msg = "我説:" + check_msg;

SetDlgNewMessage(check_msg);//设置m_cstrMsg中为新消息

SetDlgItemText(m_opControl.hWnd, IDC_CHAT_CONTENT, m_cstrMsg);

}

else

{

check_msg = sendName + "説:" + check_msg;

SetDlgNewMessage(check_msg);//设置m_cstrMsg中为新消息

SetDlgItemText(m_opControl.hWnd, IDC_CHAT_CONTENT, m_cstrMsg);

}

break;

}

//从系统返回来的错误信息

//像是服务器退出消息或是用户名重复消息。因为在未登录时,本地并无其他用户的信息,无法在本地完成判断,所以由服务器判断

case MESSAGE_SYSTEM_ERROR:

{

m_opControl.cButtonConnect->EnableWindow(TRUE);//设置连接键可点击

m_opControl.cButtonSend->EnableWindow(FALSE);//设置发送键不可点击

m_opControl.cButtonSendToAll->EnableWindow(FALSE);//设置发送所有人键不可点击

::MessageBox(m_opControl.hWnd, CA2W(check_msg.c_str()), L"错误提示", MB_ICONERROR);

break;

}

//这是系统消息

//即系统对所有用户发的消息。

case MESSAGE_SYSTEM:

{

check_msg = "系统通知:" + check_msg;

SetDlgNewMessage(check_msg);

SetDlgItemText(m_opControl.hWnd, IDC_CHAT_CONTENT, m_cstrMsg);

break;

}

}

}

同样,在析构函数中向服务器发送下线通知:

-----------------------------------------------------------

Myusock::USockClient::~USockClient()

{

std::string userName = m_userName;

FormatMsg(MESSAGE_LOGOUT, userName);

SendToServer(userName);//发送退出消息

closesocket(m_sock);

WSACleanup();

}

这里,接收函数也是需要循环接收的,所以自然也需要开个线程:

------------------------------------------------------------------

void RecvMsg(HWND hWnd)

{

while (true)

{

std::mutex mtx;

std::lock_guard lock(mtx);

try {

usockClient.RecvFromServer();

}

catch(const char *e){

::MessageBox(hWnd, CA2W(e), L"错误提示", MB_ICONERROR);

}

}

}

这里直接写在了MFC的窗口类中,所以不需要加static了,我们在里面调用:

------------------------------------------------------------

void CUdpSockClientDlg::OnBnClickedBtnConnect()

{

CString cstrAddress, cstrPort, cstrName;

GetDlgItemText(IDC_IPADDRESS1, cstrAddress);//获取输入的IP

GetDlgItemText(IDC_PORT, cstrPort);//获取输入的端口

GetDlgItemText(IDC_NICKNAME, cstrName);//获取用户昵称

if (cstrAddress.IsEmpty() || cstrPort.IsEmpty() || cstrName.IsEmpty())

{

AfxMessageBox(L"请输入IP和端口及昵称");

return;

}

if (!cstrName.Compare(L"系统消息"))

{

AfxMessageBox(L"不能使用系统名称!");

return;

}

USES_CONVERSION;

std::string strAddress = W2CA(cstrAddress);//转换后的IP

cstrPort.TrimLeft();

cstrPort.TrimRight();

unsigned int usPort = _ttoi(cstrPort);//转换后的端口

std::string strName = W2CA(cstrName);//转换后的昵称

try {

Myusock::OP_CONTROL opControl;

opControl.hWnd = GetSafeHwnd();

opControl.cListBox = &m_userList;

opControl.cButtonConnect = &m_btnConnect;

opControl.cButtonSend = &m_btnSend;

opControl.cButtonSendToAll = &m_btnSendToAll;

usockClient.Connect(opControl, strAddress, usPort, strName);

usockClient.ConnectedSocket();

usockClient.SendUserInfo();

m_btnSend.EnableWindow(TRUE);//设置发送键可点击

m_btnSendToAll.EnableWindow(TRUE);//设置发送给所有人键可用

m_btnConnect.EnableWindow(FALSE);//设置连接键不可点击

std::thread recvThread(RecvMsg, GetSafeHwnd());//开始接收线程

recvThread.detach();

}

catch (const char *e)

{

::MessageBox(GetSafeHwnd(), CA2W(e), L"错误提示", MB_ICONERROR);

}

}

发送按钮只需调用SendToOther()便可以了,发送人的名称是通过点击列表框中的用户名获取的,这是MFC的操作,便不说了。

------------------------------------------------------------

void CUdpSockClientDlg::OnBnClickedBtnSend()

{

int index = m_userList.GetCurSel();

if (index == -1)

{

AfxMessageBox(L"请选择要发送的用户");

return;

}

CString cstrName, cstrMsg;

m_userList.GetText(index, cstrName);//获取选择目标用户昵称

GetDlgItemText(IDC_EDIT_MSG, cstrMsg);//获取要发送的消息

if (cstrMsg.IsEmpty())

{

AfxMessageBox(L"请输入要发送的消息.");

return;

}

USES_CONVERSION;

std::string strName(W2CA(cstrName));

std::string strMsg(W2CA(cstrMsg));

try {

usockClient.SendToOther(strName, strMsg);

}

catch (const char *e)

{

::MessageBox(GetSafeHwnd(), CA2W(e), L"错误提示", MB_ICONERROR);

}

}

发送给所有人也只用调用SendToAll函数便好了,这里不用获取发送目标的名称了:

--------------------------------------------------------

void CUdpSockClientDlg::OnBnClickedChatToAll()

{

CString cstrMsg;

GetDlgItemText(IDC_EDIT_MSG, cstrMsg);

USES_CONVERSION;

std::string strMsg(W2CA(cstrMsg));

try {

usockClient.SendToAll(strMsg);

}

catch (const char *e) {

::MessageBox(GetSafeHwnd(), CA2W(e), L"错误提示", MB_ICONERROR);

}

}

好了,这便是基于UDP的聊天客户端的实现。

  • 发表于:
  • 原文链接:https://kuaibao.qq.com/s/20180630G02E5700?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券