C++实现流式socket聊天程序

目录

协议设计

消息的类型

消息的语法

消息的语义

消息的处理

发送消息

接收消息

程序设计

模块的划分和功能

Client客户端

Server服务器

模块流程图

程序实现

辅助代码

client.cpp

server.cpp

程序测试


本文设计并实现了使用流式socket完成双人聊天程序,支持随时发送和接收消息。在实验中遇到的主要问题及解决方案有:

  • 创建一个线程负责接收消息,解决了因为recv是阻塞函数而造成主线程等待,只能发送和接收消息接替进行的问题;
  • 引入flag表示服务器或客户端是否在线,统一了接收消息的线程和主线程,实现了接收消息的线程收到对方下线的消息后主线程也能自动结束;
  • 每次写入前清空发送和接收消息的缓冲区,避免由于前一次缓冲区溢出而造成后一次发送和接收消息异常。

如果你也遇到了同样的bug,请继续看下去吧。

协议设计

消息的类型

  1. chat:服务器与客户端之间的普通聊天信息;
  2. exit:客户端断开与服务器的连接;
  3. offline:服务器下线。

消息的语法

消息的结构如下所示:

struct message {
    int type;
    string time;
    string msg;
};

其中,string类型的字段使用的是ASCII码,每个字段通过'\n'分割。由于本文实现的是两人聊天程序,所设计的消息的结构比较简单,如果是多人聊天程序,可以增加'from', ‘to’等字段。

消息的语义

  1. type:表示消息的类型;
  2. time:表示发送消息时的本地时间,格式为年-月-日 时:分:秒;
  3. msg:信息的具体内容,例如聊天的内容等。

消息的处理

发送消息

  1. 随时可以输入要发送的消息,按回车键视为一条完整的消息结束;
  2. 将一条完整的消息存入我们定义的message结构体;
  3. 将message结构体的字段转换为字符串数组,字段之间通过'\n'分割;
  4. 向对方发送消息;
  5. 如果消息类型为exit,在命令行中打印出提示消息,关闭相关的socket,客户端断开连接;
  6. 如果消息类型为offline,在命令行中打印出提示消息,等待客户端回复‘exit’的消息后,服务器才下线。

接收消息

  1. 将字符数组类型的消息解析为我们定义的message结构体;
  2. 如果消息类型为chat,在命令行中按照时间-发送者-消息内容的格式打印出消息;
  3. 如果消息类型为exit,在命令行中打印出提示消息,关闭相关的socket,并且服务器下线(多人聊天程序则不用下线);
  4. 如果消息类型为offline,在命令行中打印出提示消息,关闭相关的socket,客户端断开连接,并向服务器发送exit消息类型。

程序设计

模块的划分和功能

Client客户端

  1. 加载环境并创建流式socket;
  2. 连接服务器:连接服务器并输出提示;
  3. 发送消息:接收通过命令行输入的消息,通过时间戳计算出发送消息的时间,判断消息类型,将消息存入定义的message结构体,并向服务器发送消息;
  4. 接收消息:创建线程,接收消息并进行相关处理,避免在主函数中调用recv函数造成主线程阻塞;
  5. 关闭socket并清理环境。

Server服务器

  1. 加载环境并创建流式socket;
  2. 绑定ip地址和服务器;
  3. 监听:等待客户端连接;
  4. 接收客户端的连接:连接服务器并输出提示;
  5. 创建新的socket:接收客户端的连接后,创建新的socket;
  6. 发送消息:接收通过命令行输入的消息,通过时间戳计算出发送消息的时间,判断消息类型,将消息存入定义的message结构体,并向客户端发送消息;
  7. 接收消息:创建线程,接收消息并进行相关处理,避免在主函数中调用recv函数造成主线程阻塞;
  8. 关闭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;
}

程序测试

对程序进行测试,可以看到服务器和客户端可以正常通信,通信消息带有时间标签,并且可以正常退出。

  • 6
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值