需求
设计一个位于消息收听端和消息广播端的用户列表,用来存放所有已连接客户的SOCKET
,并且根据send()
情况对该列表进行更新,同时,广播时也应该屏蔽该条消息的来源客户。
设计
为了记载用户的套接字及其地址信息,采用std::map<SOCKET, sockaddr_in>
的容器建立映射。为了封装这么一个容器,我写了一个简单的类。
由于在广播过程中需要迭代获取每一位当前用户,所以需要为这个类设计一个迭代接口。
class CClientInfo {
std::map<SOCKET, sockaddr_in> map; // 容器本身
size_t sz; // 容器大小限制
bool in_iter; // 用来控制迭代过程
public:
CClientInfo(u_int sz = 256);
bool Get(SOCKET& s, sockaddr_in& addr); // 传入SOCKET,返回其地址结构
bool Put(SOCKET& s, sockaddr_in& addr); // 置入新用户
void Toss(SOCKET& s, std::map<SOCKET, sockaddr_in>::iterator& it); // 在迭代过程中删除用户
void Toss(SOCKET& s); // 在非迭代过程中删除用户
bool Iter(std::map<SOCKET, sockaddr_in>::iterator& it); // 迭代器接口
bool isFull();
bool isEmpty();
};
为什么需要两种删除用户的函数,这是因为在接收端也可以根据recv()
的返回结果,判定一名已知用户已经断开连接。此时也可以调用删除用户的函数,属于在非迭代过程中删除。
而在广播端,由于广播的过程是依靠迭代实现,所以此时根据send()
结果删除已掉线客户,属于在迭代过程中删除。
重点比较两种不同的删除接口的设计方法,需要注意的点是,如何避免迭代器因为不适当的擦除过程而失效。迭代器失效会导致迭代出错。
另一个前提是,我为需要用到该迭代方法的类中设计了一个迭代器成员,通过引用传值,将迭代器送入接口,接口修改迭代器使迭代器指向某一元素。
迭代方法
bool CClientInfo::Iter(std::map<SOCKET, sockaddr_in>::iterator& it)
{
if (in_iter) { // 若此时在迭代过程中
if (++it == map.end()) {
in_iter = false; // 当迭代完成时,函数返回false,退出迭代
}
}
else { // 若此时不在迭代过程中
it = map.begin();
in_iter = true; // 进入迭代
}
return in_iter;
}
删除方法
void CClientInfo::Toss(SOCKET& s, std::map<SOCKET, sockaddr_in>::iterator& it) {
// 在迭代中删除元素时要小心迭代器失效
if (it == map.begin()) {
in_iter = false; // 如果被删除元素在开头,可以先退出循环,随后再进入循环。
}
else {
--it; // 擦除前先将迭代器指向该元素之前的一个元素,擦除后,再上述迭代过程中++it会指向该被删除元素的下一个元素,跳过该元素,放置迭代器因为擦除而失效。
}
map.erase(s);
}
void CClientInfo::Toss(SOCKET& s) {
map.erase(s);
}
广播类中的方法
注意需要自己声明一个迭代器用于传入
std::map<SOCKET, sockaddr_in>::iterator it;
将产品库声明再全局中,方便不同的类直接访问
CClientInfo cInfo
(实际上在实践过程中,我出现了重复声明的问题,无法用extern
解决,目前尚不清楚原因,最后用到了__declspec(selectany)
语句来声明变量,这在官方的文档里是不被推荐的。)
迭代过程如下:
...
while (cInfo.Iter(it)) {
iResult = send(it->first, MsgBuf, strlen(MsgBuf), 0);
if (iResult == SOCKET_ERROR) { // 如果send()结果指示该客户已掉线的处理方法
SOCKET socktemp = it->first;
shutdown(socktemp, SD_BOTH);
cInfo.Toss(socktemp, it); // it->first带有const属性,所以需要一个临时变量来辅助传值。
}
}
验证
由于目前客户端还不成熟,所以我将该方法复制到了一个简单的测试例子中,进行测试。
类的定义
#include <iostream>
#include <map>
using namespace std;
class myMap {
map<int, char> m;
bool in_iter;
public:
myMap() {
in_iter = false;
}
void Put(int& i, char& c) {
m[i] = c;
}
void Toss(int& i, map<int, char>::iterator& it) {
if (it == m.begin()) {
in_iter = false;
}
else {
--it;
}
m.erase(i);
}
void Toss(int& i) {
m.erase(i);
}
bool Iter(map<int, char>::iterator& it) {
if (in_iter) {
if (++it == m.end()) {
in_iter = false;
}
}
else {
it = m.begin();
in_iter = true;
}
return in_iter;
}
};
测试过程
首先将对象声明在全局中,然后再主函数中给产品库中传值若干次。随后进行一次迭代过程,展示迭代效果,最后进行迭代中删除元素的测试。
myMap M;
int main() {
int i = 0;
char c = 'a';
M.Put(i, c);
i = 1;
c = 'b';
M.Put(i, c);
i = 2;
c = 'c';
M.Put(i, c);
i = 3;
c = 'd';
M.Put(i, c);
map<int, char>::iterator it;
while (M.Iter(it)) {
cout << it->first << " is " << it->second << endl;
}
while (M.Iter(it)) {
if (it->second == 'a') {
int temp = it->first;
M.Toss(temp, it);
}
else {
cout << it->first << " is " << it->second << endl;
}
}
return 0;
}
第二次迭代的预期输出结果应为从'b'
到'd'
,因为首位元素应被删除。
若将第二次迭代中擦除条件改为'd'
,应输出'a'
到'c'
。
结论
太好啦!