目录
本文设计并实现了使用流式socket完成双人聊天程序,支持随时发送和接收消息。在实验中遇到的主要问题及解决方案有:
- 创建一个线程负责接收消息,解决了因为recv是阻塞函数而造成主线程等待,只能发送和接收消息接替进行的问题;
- 引入flag表示服务器或客户端是否在线,统一了接收消息的线程和主线程,实现了接收消息的线程收到对方下线的消息后主线程也能自动结束;
- 每次写入前清空发送和接收消息的缓冲区,避免由于前一次缓冲区溢出而造成后一次发送和接收消息异常。
如果你也遇到了同样的bug,请继续看下去吧。
协议设计
消息的类型
- chat:服务器与客户端之间的普通聊天信息;
- exit:客户端断开与服务器的连接;
- offline:服务器下线。
消息的语法
消息的结构如下所示:
struct message {
int type;
string time;
string msg;
};
其中,string类型的字段使用的是ASCII码,每个字段通过'\n'分割。由于本文实现的是两人聊天程序,所设计的消息的结构比较简单,如果是多人聊天程序,可以增加'from', ‘to’等字段。
消息的语义
- type:表示消息的类型;
- time:表示发送消息时的本地时间,格式为年-月-日 时:分:秒;
- msg:信息的具体内容,例如聊天的内容等。
消息的处理
发送消息
- 随时可以输入要发送的消息,按回车键视为一条完整的消息结束;
- 将一条完整的消息存入我们定义的message结构体;
- 将message结构体的字段转换为字符串数组,字段之间通过'\n'分割;
- 向对方发送消息;
- 如果消息类型为exit,在命令行中打印出提示消息,关闭相关的socket,客户端断开连接;
- 如果消息类型为offline,在命令行中打印出提示消息,等待客户端回复‘exit’的消息后,服务器才下线。
接收消息
- 将字符数组类型的消息解析为我们定义的message结构体;
- 如果消息类型为chat,在命令行中按照时间-发送者-消息内容的格式打印出消息;
- 如果消息类型为exit,在命令行中打印出提示消息,关闭相关的socket,并且服务器下线(多人聊天程序则不用下线);
- 如果消息类型为offline,在命令行中打印出提示消息,关闭相关的socket,客户端断开连接,并向服务器发送exit消息类型。
程序设计
模块的划分和功能
Client客户端
- 加载环境并创建流式socket;
- 连接服务器:连接服务器并输出提示;
- 发送消息:接收通过命令行输入的消息,通过时间戳计算出发送消息的时间,判断消息类型,将消息存入定义的message结构体,并向服务器发送消息;
- 接收消息:创建线程,接收消息并进行相关处理,避免在主函数中调用recv函数造成主线程阻塞;
- 关闭socket并清理环境。
Server服务器
- 加载环境并创建流式socket;
- 绑定ip地址和服务器;
- 监听:等待客户端连接;
- 接收客户端的连接:连接服务器并输出提示;
- 创建新的socket:接收客户端的连接后,创建新的socket;
- 发送消息:接收通过命令行输入的消息,通过时间戳计算出发送消息的时间,判断消息类型,将消息存入定义的message结构体,并向客户端发送消息;
- 接收消息:创建线程,接收消息并进行相关处理,避免在主函数中调用recv函数造成主线程阻塞;
- 关闭socket并清理环境。
模块流程图
程序实现
辅助代码
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <WinSock2.h>
#include <string>
#include <time.h>
#pragma comment(lib,"ws2_32.lib")
// 表示是否在线,0表示下线,1表示在线
int flag = 0;
// 消息类型
enum Type {
CHAT = 1,
EXIT,
OFFLINE
};
// 自定义消息结构
struct message {
Type type;
string time;
string msg;
};
// message类型转换为string类型,字段间以'\n'为分隔符
string msgToString(message m) {
string s;
if (m.type == CHAT) {
s = '1';
}
else if (m.type == EXIT) {
s = '2';
}
else if (m.type == OFFLINE) {
s = '3';
}
s.append("\n");
s.append(m.time);
s.append("\n");
s.append(m.msg);
s.append("\n");
return s;
}
// string类型转换为message类型,字段间以'\n'为分隔符
message stringToMsg(string s) {
message m;
if (s[0] == '1') {
m.type = CHAT;
}
else if (s[0] == '2') {
m.type = EXIT;
}
else if (s[0] == '3') {
m.type = OFFLINE;
}
int i = 2;
while (s[i] != '\n') {
i++;
}
m.time = s.substr(2, i - 2);
m.msg = s.substr(i + 1);
return m;
}
client.cpp
// 负责接收消息的线程
DWORD WINAPI clientThread(LPVOID IpParameter) {
SOCKET sockClient = *(SOCKET*)IpParameter;
// 接收消息的缓冲区,记得每次要清空
char recvBuf[1024];
memset(recvBuf, 0, sizeof(recvBuf));
int recvLen = 0;
// 当客户端还在线时,持续接收消息
while (flag) {
recvLen = recv(sockClient, recvBuf, 1024, 0);
if (recvLen > 0) {
string s = recvBuf;
message r = stringToMsg(s);
message m;
char tmp[32] = { NULL };
time_t t;
switch (r.type){
// 输出聊天消息
case 1:
cout << r.time << " " << "Server: " << r.msg;
break;
// 服务器下线,客户端向服务器发送断开连接的消息,并下线
case 3:
cout << "---------------------------------------------------" << endl;
cout << "[SERVER IS OFFLINE! PRESS ENTER TO CLOSE CONNECTION!]" << endl;
m.type = EXIT;
t = time(0);
strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S", localtime(&t));
m.time = tmp;
m.msg = "Client closed connection.";
char sendBuf[1024];
memset(sendBuf, 0, sizeof(sendBuf));
strcpy_s(sendBuf, msgToString(m).c_str());
send(sockClient, sendBuf, sizeof(sendBuf), 0);
flag = 0;
break;
default:
break;
}
}
memset(recvBuf, 0, sizeof(recvBuf));
}
return 0;
}
int main() {
// 加载环境
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
cout << "WSA STARTUP ERROR:" << GetLastError() << endl;
return 0;
}
else {
cout << "[WSA STARTED UP!]" << endl;
}
// 创建流式套接字
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
if (sockClient == INVALID_SOCKET) {
cout << "SOCKET ERROR:" << GetLastError() << endl;
return 0;
}
else {
cout << "[SOCKET BUILT!]" << endl;
}
// 连接服务器
sockaddr_in addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(8000);
addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (connect(sockClient, (SOCKADDR*)&addrServer, sizeof(SOCKADDR))) {
cout << "CONNECT ERROR:" << GetLastError() << endl;
return 0;
}
else {
cout << "[CONNECTION BUILT! ENTER exit TO CLOSE CONNECTION!]" << endl;
cout << "---------------------------------------------------" << endl;
flag = 1;
}
// 接收和发送消息
char recvBuf[1024];
char sendBuf[1024];
memset(recvBuf, 0, sizeof(recvBuf));
memset(sendBuf, 0, sizeof(sendBuf));
message m;
// 创建线程负责接收消息
CloseHandle(CreateThread(NULL, 0, clientThread, (LPVOID)&sockClient, 0, 0));
// 当客户端还在线时,随时可以发送消息
while (flag) {
char in[1000];
cin.getline(in, 1000);
m.msg = in;
// 通过时间戳计算发送时间
char tmp[32] = { NULL };
time_t t = time(0);
strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S", localtime(&t));
m.time = tmp;
if (m.msg == "exit") {
m.type = EXIT;
}
else {
m.type = CHAT;
}
strcpy_s(sendBuf, msgToString(m).c_str());
send(sockClient, sendBuf, sizeof(sendBuf), 0);
memset(sendBuf, 0, sizeof(sendBuf));
// 输入exit断开连接
if (m.msg == "exit") {
cout << "---------------------------------------------------" << endl;
cout << "[CONNECTION CLOSED!]" << endl;
flag = 0;
break;
}
/*
recvLen = recv(sockClient, recvBuf, 1024, 0);
if (recvLen > 0) {
cout << recvBuf << endl;
}
memset(recvBuf, 0, sizeof(recvBuf));
*/
}
// 关闭监听套接字
closesocket(sockClient);
cout << "[SOCKET CLOSED!]" << endl;
//清理环境
WSACleanup();
cout << "[WSA CLEANED UP!]" << endl;
system("pause");
return 0;
}
server.cpp
DWORD WINAPI serverThread(LPVOID IpParameter) {
SOCKET sockConn = *(SOCKET*)IpParameter;
char recvBuf[1024];
memset(recvBuf, 0, sizeof(recvBuf));
int recvLen = 0;
while (flag) {
recvLen = recv(sockConn, recvBuf, 1024, 0);
if (recvLen > 0) {
string s = recvBuf;
message r = stringToMsg(s);
switch (r.type) {
case 1:
cout << r.time << " " << "Client: " << r.msg;
break;
case 2:
cout << "---------------------------------------------------" << endl;
cout << "[CLIENT CLOSED CONNECTION! PRESS ENTER TO CLOSE CONNECTION!]" << endl;
flag = 0;
break;
default:
break;
}
}
memset(recvBuf, 0, sizeof(recvBuf));
}
return 0;
}
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
cout << "WSASTARTUP ERROR:" << GetLastError() << endl;
return 0;
}
else {
cout << "[WSA STARTED UP!]" << endl;
}
SOCKET sockServer = socket(AF_INET, SOCK_STREAM, 0);
if (sockServer == INVALID_SOCKET) {
cout << "SOCKET ERROR:" << GetLastError() << endl;
return 0;
}
else {
cout << "[SOCKET BUILT!]" << endl;
}
// 绑定ip地址和服务器
sockaddr_in addrServer;
memset(&addrServer, 0, sizeof(sockaddr_in));
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(8000);
addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (bind(sockServer, (SOCKADDR*)&addrServer, sizeof(SOCKADDR)) == SOCKET_ERROR) {
cout << "BIND ERROR:" << WSAGetLastError() << endl;
return 0;
}
else {
cout << "[BINDED. SERVER UP!]" << endl;
flag = 1;
}
// 监听
listen(sockServer, 5);
cout << "[LISTENING!]" << endl;
// 当服务器在线时,接收客户端的连接请求
while (flag) {
sockaddr_in addrClient;
int len = sizeof(sockaddr_in);
// 接收客户端的连接请求并创建新的套接字
SOCKET sockConn = accept(sockServer, (SOCKADDR*)&addrClient, &len);
if (sockConn != INVALID_SOCKET) {
cout << "[ACCEPT CONNECTION REQUEST! ENTER off TO CLOSE CONNECTION!]" << endl;
cout << "---------------------------------------------------" << endl;
char recvBuf[1024];
char sendBuf[1024];
memset(recvBuf, 0, sizeof(recvBuf));
memset(sendBuf, 0, sizeof(sendBuf));
message m;
CloseHandle(CreateThread(NULL, 0, serverThread, (LPVOID)&sockConn, 0, 0));
while (flag) {
char in[1000];
cin.getline(in, 1000);
m.msg = in;
char tmp[32] = { NULL };
time_t t = time(0);
strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S", localtime(&t));
m.time = tmp;
if (m.msg == "off") {
m.type = OFFLINE;
}
else {
m.type = CHAT;
}
strcpy_s(sendBuf, msgToString(m).c_str());
send(sockConn, sendBuf, sizeof(sendBuf), 0);
memset(sendBuf, 0, sizeof(sendBuf));
if (m.msg == "off") {
cout << "---------------------------------------------------" << endl;
cout << "[CONNECTION CLOSED!]" << endl;
flag = 0;
break;
}
}
}
closesocket(sockConn);
}
closesocket(sockServer);
cout << "[SOCKET CLOSED!]" << endl;
WSACleanup();
cout << "[WSA CLEANED UP!]" << endl;
system("pause");
return 0;
}
程序测试
对程序进行测试,可以看到服务器和客户端可以正常通信,通信消息带有时间标签,并且可以正常退出。