网络聊天室——掌赢在线笔试

本文介绍了作者参与的掌赢信息科技在线笔试,主要任务是设计并实现一个网络聊天室服务端,要求使用TCP,支持NIO模式,超时断开,昵称唯一,并具备天气查询功能。客户端通过telnet连接,输入特定命令可以查询天气。此外,还讨论了程序设计思路,潜在的改进点及应对大规模用户的方法。
摘要由CSDN通过智能技术生成

    最近做了掌赢信息科技(上海)有限公司的一个在线笔试题,服务端机试题目——网络聊天室,比较开放,给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. 几点说明

按照上面的要求,基本完成,由于时间有限,也是因为自己不熟,有几条没能实现:
1)客户端是自己同样利用TCP编写的,没有按照要求用telnet连接至客户端;因为不是很熟,尝试过有些小问题,时间紧迫,所以选择放弃。
2)没有使用NIO模式,这个模式在JAVA中有封装好的包可以使用,也是因为不熟,没时间折腾,所以选择放弃。
3)程序里没有考虑10万以上级别客户端高并发登陆的情况,请高手指教!
4)如果系统支撑的用户数量扩大100倍,如何处理?请高手可以指教一二!

2. 服务器与客户端设计


    Windows 7   32位
    采用的是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类,后面着重实现查询派生类即可,对其他模块修改的少。当然也可以利用模板类或者模板函数来实现多态。

内容还是比较多的,由于时间关系,这里说得相当简单,具体请看下源码,有注释说明。


源码程序

                      exe:http://download.csdn.net/detail/ningsoft/8759733

                      源码:







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值