最近做了掌赢信息科技(上海)有限公司的一个在线笔试题,服务端机试题目——网络聊天室,比较开放,给24个小时进行答题,虽然就一个题目,但是对于我来说,量还是挺大的,对方解释主要是看笔试者的代码风格习惯。花了点时间完成了基本的要求,现将题目记录如下。
题目
服务端机试题目(Java/C/C++/Go)
1. 实现一个简单的网络聊天室服务端,功能要求如下:
1) 实现服务端(命令行即可,不需要图像UI,使用TCP);
2) 客户端通过telnet连接至服务端;
3) 使用NIO模式;
4) 超时处理:用户超过60S无输入,自动断开连接;
5) 客户端初次连接时服务端返回提示信息,提示用户输入用户昵称;用户输入昵称后,按回车键发送至服务器,如果此昵称已经有其他人用,提示重新输入;如果昵称唯一,则登录;
6) 客户端登录后,输入任何abcd等普通字符,按回车键发送至服务器;当客户端输入‘/quit’并回车时('/'表示此输入是命令),断开连接;
7) 服务器接收客户登录,保证客户昵称唯一即可;用户登录后发送已经设置好的欢迎信息和在线人数给客户端;
8) 服务器收到已登录用户的输入内容,然后在内容前面加上发送方的昵称和分号,转发至其他所有登录客户端,不回发给发送此内容的客户端;
9) 有客户端上线或者下线时,发送通知到所有在线用户,通知内容为“xx已上线”,“xx已下线”;
10) 考虑服务端如果需要支持10万以上级别客户端登录的情况;
11) 各种数据不需要存磁盘;
12) 请看完下一题再开始写代码;
2. 在上一题的基础上,实现自动查询机器人服务,功能要求如下:
1) 实现天气查询服务,客户端输入:"/天气 北京",服务端调用接口获取北京天气信息返回给客户端;第一个词为查询词('/'表示此输入是命令),后一个为查询关键词,查询类的输入内容,不用发给其他用户;
2) 程序结构需要设计灵活,将来增加查询股票、火车票、飞机票等功能;比如查询火车票的查询格式为:"/火车票 北京 上海",即可查询北京到上海的火车票情况;
3) 需要有缓存机制,比如城市天气信息,相同城市缓存2小时,减少对接口的调用次数;
4) 如果需要实现查询功能的动态增减,给出解决办法说明,不需要写出代码;
3. 给出文档说明,要求如下:
1) 程序设计的思路;
2) 系统还有哪些改进点;
3) 如果系统支撑的用户数量扩大100 倍,如何处理;
注意:此机试答案需提交代码(上传压缩包)和文档
程序实现
1. 几点说明
2. 服务器与客户端设计
采用的是winsock2
时间关系,没有画图,抱歉!
【服务器】首先,用到的是阻塞的send和recv。按照常规设计,监听套接字启动以后,客户端连接上之后,立马为客户端建立一个线程,在线程中对用户名进行验证,保证其唯一性,若输入名重复,则一直阻塞于待登陆状态,若用户强关客户端,则线程结束。登陆成功后,给客户端发送提示信息,同时给其它用户发送该用户上线提示,之后进入聊天模式,每当接收到用户发来的信息,会群发给其他用户,当用户输入‘/quit’时或者强关时,同时,若超时60s无输入,会关闭线程,并发送此用户下线消息给所有用户(包括自己)。
#include "stdafx.h"
#include "User.h"
#include <iostream>
#include <winsock2.h>
#include <vector>
#include <regex>
#include "Message.h"
#include "Query.h"
#include "Weather.h"
using namespace std;
#pragma comment(lib, "ws2_32.lib")
#define MAXSIZE 2048
#define PORT 6001 // 服务器端口号
#define TIMELIMIT (2*60*60*1000) // 天气查询缓存时间设定
SOCKET ServerSocket, CientSocket; // 套接字
sockaddr_in servAddr; // 服务器网际网套接字地址结构
sockaddr_in cliAddr; // 客户端网际网套接字地址结构
MsgInfo recvMsgInfo; // 接收到的客户端用户数据信息
vector<CUser> onlineUsers; // 在线用户数据集
int addrLen; // 地址长度
vector<sWeather> vecWeather; // 存放天气数据缓冲数据
CQuery *query;
CWeather *weatherQuery;
// 服务器创建的聊天线程
DWORD WINAPI ClientThread(LPVOID lpParameter)
{
SOCKET CientSocket = (SOCKET)lpParameter;
int ret = 0;
char RecvBuffer[MAXSIZE];
// 先验证用户名
string name;
send(CientSocket, "请输入用户名:", 100, 0);
while (true)
{
memset(RecvBuffer, 0x00, sizeof(RecvBuffer));
ret = recv(CientSocket, RecvBuffer, MAXSIZE, 0);
name = RecvBuffer;
if ("" == name) // 当用户没输入账号直接关闭客户端时,name就会为空,算是一个异常(后面再考虑统一写一个异常处理类,方便扩展程序)
{
return 0;
}
else
{
if (CUser::findUser(name, onlineUsers) != NULL)
{
CMessage::sendMessage("login", "此用户名已存在,请输入用户名:", CientSocket);
continue;
}
else
{
break;
}
}
}
// 登陆成功发送欢迎内容和在线人数
char c[10];
_itoa_s(onlineUsers.size() + 1, c, 10); // int转char[]
string sn(c); // char转string
string wellcom = "==================欢迎【" + name + "】成功加入聊天==================\n目前在线人数:" + sn + "\0";
CMessage::sendMessage("wellcom", wellcom, CientSocket); // 发送欢迎信息和在线人数
// 将新登录的用户加入到在线用户集
CUser::addUserToOnline(name, cliAddr.sin_addr, cliAddr.sin_port, CientSocket, onlineUsers);
// 接收用户聊天信息
static bool isFirst = true;
while (true)
{
if (isFirst)
{
string str = "用户【" + name + "】已上线!";
CMessage::sendGroupMessage("userOnline", str, name, onlineUsers); // 向其他用户群发此用户下线提示消息
isFirst = false;
cout << str << endl;
}
// 此条存在于服务器的聊天线程对用户发来的信息进行接收
memset(RecvBuffer, 0x00, sizeof(RecvBuffer));
ret = recv(CientSocket, RecvBuffer, MAXSIZE, 0); // 此时接收的的不是结构体数据,是字符串类型
string tmp(RecvBuffer);
// 判断是否是查询命令
vector<string> vec;
string space = " ";
query->split(tmp,space,&vec);
// 向其他用户群发聊天内容 或者向用户发送查询信息
if (ret == 0 || ret == SOCKET_ERROR || "/quit" == tmp)
{
CUser::deleteUser(CientSocket, onlineUsers); // 从在线用户中删除下线用户
string str = "用户【" + name + "】已下线!";
CMessage::sendGroupMessage("userOffline", str, name, onlineUsers); // 向其他用户群发此用户下线提示消息
cout << str << endl;
break;
}
else if ("/天气" == vec[0] && vec.size() > 1) // 可以不用判断vec是否为空,不存在这种情况
{
sWeather* cw = weatherQuery->findCityWeather(vec[1], vecWeather, TIMELIMIT);
if (NULL != cw)
{
CMessage::sendMessage("weather", cw->weatherInfo, CientSocket); // 发送查询的天气信息
}
else{
CMessage::sendMessage("weather", "【提示】:未能查询到天气信息!\n可能出现的情况:1)查询命令出错(如城市不存在等) 2)天气查询服务器不稳定没有返回值\n请重新输入查询命令!", CientSocket);
}
}
else{
CMessage::sendGroupMessage(name, tmp, name, onlineUsers);
}
cout << name << ":" << RecvBuffer << endl;
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
weatherQuery = new CWeather();
query = weatherQuery;
// 服务器启动时,清空在线用户数据
onlineUsers.clear();
WSADATA Ws; // Windows Sockets初始化信息
int ret = 0; // 绑定、监听操作等返回值
HANDLE hThread = NULL; // 聊天线程
char RecvBuffer[MAXSIZE]; // 接收客户端的数据
char SendBuffer[MAXSIZE]; // 发送给客户端的数据
// 1. 启动winsock操作
if (WSAStartup(MAKEWORD(2, 2), &Ws) != 0)
{
cout << "Windows Sockets初始化失败!" << GetLastError() << endl;
return -1;
}
// 2. 创建TCP套接字socket
ServerSocket = socket(AF_INET, SOCK_STREAM, 0);
if (ServerSocket == INVALID_SOCKET)
{
cout << "套接字创建失败!" << GetLastError() << endl;
return -1;
}
// 3. 参数绑定
servAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置通配地址:此宏表示本地的任意IP地址(可能有多个网卡),即在所有的IP地址上监听
servAddr.sin_family = AF_INET; // 设置地址类型为AF_INET
servAddr.sin_port = htons(PORT); // 设置服务器端口
memset(servAddr.sin_zero, 0x00, 8); // 将整个结构体清零
// 4. 将TCP套接字与地址绑定
addrLen = sizeof(servAddr);
ret = bind(ServerSocket, (sockaddr*)&servAddr, addrLen);
if (ret != 0)
{
cout << "套接字与地址绑定失败!" << GetLastError() << endl;
return -1;
}
// 5. 开始侦听:把套接字转换为一个监听套接字,即声明listenfd处于监听状态
ret = listen(ServerSocket, 5); // 等待队列最大成员数5
if (ret != 0)
{
cout << "监听套接字启动失败!" << GetLastError() << endl;
return -1;
}
cout << "服务端已经启动" << endl;
// 6. 接收客户端发来的连接请求
while (true)
{
addrLen = sizeof(cliAddr);
// 握手成功后,服务器调用accept接收连接,若还无客户端连接,则阻塞直到有客户连接,返回时传回客户端的地址和端口号
CientSocket = accept(ServerSocket, (sockaddr*)&cliAddr, &addrLen);
if (CientSocket == INVALID_SOCKET)
{
cout << "无效套接字!" << GetLastError() << endl;
break;
}
cout << "客户端连接信息:" << inet_ntoa(cliAddr.sin_addr) << ":" << cliAddr.sin_port << endl;
// 创建聊天线程
hThread = CreateThread(NULL, 0, ClientThread, (LPVOID)CientSocket, 0, NULL);
if (hThread == NULL)
{
cout << "聊天线程创建失败!" << endl;
break;
}
CloseHandle(hThread);
}
closesocket(ServerSocket); // 关闭套接字
closesocket(CientSocket); // 关闭套接字
WSACleanup(); // 关闭加载的套接字库
return 0;
}
【客户端】启动后,与服务器建立连接,用户登录成功后,则创建一个服务器群发消息的接收线程,对服务器传来的消息进行接收,然后主线程接着进入一个循环函数,进行消息的发送操作。
【天气查询】在服务器与客户端处于天阶段,客户端发送“/天气 武汉”字符串到服务器,服务器利用《中央气象台的API》进行天气信息的获取,之后用JsonCPP对获得的json数据进行解析,把需要的内容发送给客户端。中间有详细的异常控制和出错提示,如命令错误、天气查询服务器不稳定问题等。
#include "stdafx.h"
#include <iostream>
#include <winsock2.h>
#include "Message.h"
using namespace std;
#pragma comment(lib, "ws2_32.lib")
#define MAXSIZE 2048
#define PORT 6001
#define IP "127.0.0.1"
SOCKET CientSocket;
sockaddr_in servAddr;
DWORD WINAPI thread(LPVOID lpParameter)
{
SOCKET CientSocket = (SOCKET)lpParameter;
int ret = 0;
char RecvBuffer[MAXSIZE];
while (true)
{
memset(RecvBuffer, 0x00, sizeof(RecvBuffer));
ret = recv(CientSocket, RecvBuffer, MAXSIZE, 0);
MsgInfo msg;
memset(&msg, 0, sizeof(MsgInfo)); // 清空结构体
memcpy(&msg, RecvBuffer, sizeof(RecvBuffer)); // 以这种方式将字符串转换成结构体数据
string name,data;
name = msg.flag;
data = msg.info;
if ("userOffline" == name || "userOnline" == name)
{ // 用户下线提示
cout << data.c_str() << endl;
}
else if ("weather" == name) // 如果是查询的天气信息
{
cout << data.c_str() << endl;
}
else{ // 其他用户聊天内容
cout << name.c_str() << ":" << data.c_str() << endl;
}
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA ws;
int ret = 0;
HANDLE hThread = NULL;
char SendBuffer[MAXSIZE];
char RecvBuffer[MAXSIZE];
// 1. 启动winsock操作
if (WSAStartup(MAKEWORD(2, 2), &ws) != 0)
{
cout << "Windows Sockets初始化失败!" << GetLastError() << endl;
return -1;
}
// 2. 创建TCP套接字socket
CientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (CientSocket == INVALID_SOCKET)
{
cout << "套接字创建失败!" << GetLastError() << endl;
return -1;
}
// 3. 参数绑定
servAddr.sin_addr.s_addr = inet_addr(IP); // 设置通配地址:此宏表示本地的任意IP地址(可能有多个网卡),即在所有的IP地址上监听
servAddr.sin_family = AF_INET; // 设置地址类型为AF_INET
servAddr.sin_port = htons(PORT); // 设置服务器端口
memset(servAddr.sin_zero, 0x00, sizeof(servAddr.sin_zero)); // 将整个结构体清零
// 4. 开始连接
ret = connect(CientSocket, (sockaddr*)&servAddr, sizeof(servAddr));
if (ret == SOCKET_ERROR)
{
cout << "Connect连接出错!" << GetLastError() << endl;
}
// 5. 与服务器沟通-->创建用户
ret = recv(CientSocket, RecvBuffer, MAXSIZE, 0); // 接收数据“请输入用户名:”
cout << RecvBuffer << endl;
cin.getline(SendBuffer, sizeof(SendBuffer)); // 输入用户名
ret = send(CientSocket, SendBuffer, (int)strlen(SendBuffer), 0);
memset(RecvBuffer, 0x00, sizeof(RecvBuffer));
ret = recv(CientSocket, RecvBuffer, (int)sizeof(RecvBuffer), 0); // 接收信息
MsgInfo msg;
memset(&msg, 0, sizeof(MsgInfo)); // 清空结构体
memcpy(&msg, RecvBuffer, sizeof(RecvBuffer)); // 以这种方式将字符串转换成结构体数据
string sflag;
sflag = msg.flag;
bool isCout = false;
while (msg.flag == "login"){ // 返回的标记flag="login",表示账号已存在,需要重新输入,知道正确的账号为止
if (!isCout)
cout << msg.info << endl;
cin.getline(SendBuffer, sizeof(SendBuffer));
ret = send(CientSocket, SendBuffer, (int)strlen(SendBuffer), 0);
memset(RecvBuffer, 0x00, sizeof(RecvBuffer));
ret = recv(CientSocket, RecvBuffer, MAXSIZE, 0);
MsgInfo msg;
memcpy(&msg, RecvBuffer, sizeof(RecvBuffer));
sflag = msg.flag;
cout << msg.info << endl;
isCout = true;
}
if (!isCout)
cout << msg.info << endl;
// 创建服务器群发消息接收线程
hThread = CreateThread(NULL, 0, thread, (LPVOID)CientSocket, 0, NULL);
if (hThread == NULL)
{
cout << "聊天线程创建失败!" << endl;
return -1;
}
CloseHandle(hThread);
// 正式开始聊天
while (true)
{
// 输入聊天内容,并向服务器发送
cin.getline(SendBuffer, sizeof(SendBuffer));
ret = send(CientSocket, SendBuffer, (int)strlen(SendBuffer), 0); // 此时发送的不是结构体数据,是字符串类型
if (ret == SOCKET_ERROR)
{
cout << "信息发送出错!" << GetLastError() << endl;
break;
}
// 当客户端输入“/quit”,表示断开连接,结束聊天
string tmp(SendBuffer);
if ("/quit" == tmp)
{
return -1;
}
}
closesocket(CientSocket); // 关闭套接字
WSACleanup(); // 关闭加载的套接字库
return 0;
}
3. 程序设计类
【服务器】1)CMessage: 通信过程中的信息类,主要封装了用于socket通信的结构体和一些方法
2)CUser:用户信息类,用户保存用户信息
3)CQuery:查询功能基类;对于可能要添加的查询功能的一些基本抽象,便于扩展功能
4)CWeather:查询天气情况的类,派生于CQuery,主要封装了天气查询的一些属性和方法
【客户端】
CMessage: 通信过程中的信息类,主要封装了用于socket通信的结构体
4. 针对查询功能
程序实现了对天气的查询,为了方便扩展,我写了一个基类,利用继承多态的形式来方便扩展,当需要添加一个新的查询功能时,只需要继承CQuery类,后面着重实现查询派生类即可,对其他模块修改的少。当然也可以利用模板类或者模板函数来实现多态。内容还是比较多的,由于时间关系,这里说得相当简单,具体请看下源码,有注释说明。