使用socket实现简单网络聊天室之客户端

基本流程

1. 初始化 WinSock: 调用 WSAStartup 初始化 WinSock 库。

    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD(2, 2);

    err = WSAStartup(wVersionRequested, &wsaData);
    if (err != 0) {                             
        return -1;
    }                                  

2. 创建套接字: 调用 socket 创建一个新的套接字。

	SOCKET hSock;
    hSock = socket(AF_INET, SOCK_STREAM, 0);

	SOCKADDR_IN servAdr;
    memset(&servAdr, 0, sizeof(servAdr));
    servAdr.sin_family = AF_INET;
    servAdr.sin_port = htons(9999);
    inet_pton(AF_INET, "139.155.130.173", &servAdr.sin_addr);

3. 连接服务器: 使用服务器的 IP 地址和端口号,通过 connect 连接到服务器。

if (connect(hSock, (SOCKADDR*)&servAdr, sizeof(servAdr))== SOCKET_ERROR) {
        std::cout << "connect error : " << GetLastError();
        return -1;
    }
    else {
        std::cout << "welcome chat room,please enter your name: ";
    }

4. 发送和接收线程:

创建两个线程,一个用于发送消息 (SendMsg),另一个用于接收消息 (RecvMsg)。

  • 发送线程从控制台读取用户输入,并将其发送到服务器。

  • 接收线程不断从服务器接收消息,并显示在控制台上。

    //循环发消息
    HANDLE hSendHand = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)SendMsg, (void*)&hSock, 0,NULL);

    //循环收消息
    HANDLE hRecvHand = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)RecvMsg, (void*)&hSock, 0, NULL);

5. 线程同步: 使用 WaitForSingleObject 等待两个线程结束。

    WaitForSingleObject(hSendHand, INFINITE);
    WaitForSingleObject(hRecvHand, INFINITE);

完整代码实现:

#include<iostream>

#include<WinSock2.h>
#include<WS2tcpip.h>

#include<windows.h>
// link with Ws2_32.lib
#pragma comment(lib,"Ws2_32.lib")
#define BUF_SIZE  1024

char szMsg[BUF_SIZE];
unsigned SendMsg(void *arg) {


    SOCKET sock = *((SOCKET*)arg);
    while (true)
    {
        //scanf_s("%s", szMsg);
        std::cin >> szMsg;
        if (!strcmp(szMsg, "QUIT\n") || !strcmp(szMsg, "quit\n")) {
            closesocket(sock);
            exit(0);
        }
        send(sock, szMsg, strlen(szMsg), 0);
    }
    return 0;
}
unsigned RecvMsg(void* arg) {


    SOCKET sock = *((SOCKET*)arg);

    char msg[BUFSIZ];
    while (true)
    {
        int len = recv(sock, msg, sizeof(msg) - 1, 0);
        if (len == -1) {
            return -1;
        }
        msg[len] = '\0';
        std::cout << msg;
    }
    return 0;
}

int main() {

    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD(2, 2);

    err = WSAStartup(wVersionRequested, &wsaData);
    if (err != 0) {
                                      
        return -1;
    }                                  

    if (LOBYTE(wsaData.wVersion) != 2 ||
        HIBYTE(wsaData.wVersion) != 2) {
                                     
        WSACleanup();
        return -1;
    }
    //创建
    SOCKET hSock;
    hSock = socket(AF_INET, SOCK_STREAM, 0);

    //绑定
    SOCKADDR_IN servAdr;
    memset(&servAdr, 0, sizeof(servAdr));
    servAdr.sin_family = AF_INET;
    servAdr.sin_port = htons(9999);
    inet_pton(AF_INET, "139.155.130.173", &servAdr.sin_addr);

    //连接服务器

    if (connect(hSock, (SOCKADDR*)&servAdr, sizeof(servAdr))== SOCKET_ERROR) {
        std::cout << "connect error : " << GetLastError();
        return -1;
    }
    else {
        std::cout << "welcome chat room,please enter your name: ";
    }

    //循环发消息
    HANDLE hSendHand = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)SendMsg, (void*)&hSock, 0,NULL);

    //循环收消息
    HANDLE hRecvHand = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)RecvMsg, (void*)&hSock, 0, NULL);
        
    //等待线程结束

    WaitForSingleObject(hSendHand, INFINITE);
    WaitForSingleObject(hRecvHand, INFINITE);



    WSACleanup();
}

存在的问题:

  1. 退出机制: 代码中使用 exit(0) 在发送 QUIT 消息后直接退出程序。这样做可能会导致接收线程在退出时没有机会进行清理。更好的方法是设置一个全局的退出标志,在检查到该标志时让两个线程安全退出。

  2. 缓冲区大小: 接收缓冲区 msg 使用的是 BUFSIZ,它可能小于发送缓冲区 szMsgBUF_SIZE。为保持一致性和避免潜在的溢出问题,这两个缓冲区大小应该相同。

  3. 输入处理: 使用 std::cin >> szMsg; 只能读取空格前的字符串,这可能无法发送包含空格的完整消息。也可以使用 std::getline(std::cin, szMsg); 来读取整行输入。

  4. 资源泄漏: 在调用 exit(0) 或发生错误时,线程可能没有正确关闭套接字就退出了,导致资源泄漏。

  5. 线程安全: 在多线程环境中操作套接字和其他共享资源时需要特别小心。虽然这个例子简单且可能不会出现立即的问题,但在更复杂的场景下可能需要考虑额外的线程同步。

  6. 硬编码的 IP 和端口: 服务器地址和端口硬编码在代码中,这样做不够灵活。更好的做法是从配置文件或命令行参数中读取这些信息。