0


C++语言实现网络聊天程序(基于TCP/IP协议的SOCKET编程)超详细(代码+解析)

一、实验目的

1、熟悉VisualC++的基本操作;掌握最基本的Client/Server(客户机/服务器)模

式的网络编程技术,并在此基础上实现更为复杂的网络编程。

2、基本了解基于对话框的windows应用程序的编写过程。

3、对于Windows Socket编程建立初步概念。

二、编程工具

Visual Studio 2022

三、实验方法

程序分为服务端和客户端两个

用户需要创建两个C++项目,分别命名为

①聊天程序服务端

②聊天程序客户端

    如图所示

同时为了避免编译软件出现c4996错误,导致编译不通过,应该鼠标右击项目列表的.CPP文件,将SDL检查,调成否。

并且点击目录最上方的“解决方案“XXXX””设置属性,将两个项目设置为同时启动

四、实验代码(附加超详细注释):

服务端:

#include"pch.h"//预编译头
#include<iostream>
#include<Winsock2.h>//socket头文件
#include<cstring>

using namespace std;

//载入系统提供的socket动态链接库

#pragma comment(lib,"ws2_32.lib")   //socket库

//==============================全局变量区===================================
const int BUFFER_SIZE = 1024;//缓冲区大小
int RECV_TIMEOUT = 10;//接收消息超时
int SEND_TIMEOUT = 10;//发送消息超时
const int WAIT_TIME = 10;//每个客户端等待事件的时间,单位毫秒
const int MAX_LINK_NUM = 10;//服务端最大链接数
SOCKET cliSock[MAX_LINK_NUM];//客户端套接字 0号为服务端
SOCKADDR_IN cliAddr[MAX_LINK_NUM];//客户端地址
WSAEVENT cliEvent[MAX_LINK_NUM];//客户端事件 0号为服务端,它用于让程序的一部分等待来自另一部分的信号。例如,当数据从套接字变为可用时,winsock 库会将事件设置为信号状态
int total = 0;//当前已经链接的客服端服务数

//==============================函数声明===================================
DWORD WINAPI servEventThread(LPVOID IpParameter);//服务器端处理线程

int main() 
{
    //1、初始化socket库
    WSADATA wsaData;//获取版本信息,说明要使用的版本
    WSAStartup(MAKEWORD(2, 2), &wsaData);//MAKEWORD(主版本号, 副版本号)

    //2、创建socket
    SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);//面向网路的流式套接字

    //3、将服务器地址打包在一个结构体里面
    SOCKADDR_IN servAddr; //sockaddr_in 是internet环境下套接字的地址形式
    servAddr.sin_family = AF_INET;//和服务器的socket一样,sin_family表示协议簇,一般用AF_INET表示TCP/IP协议。
    servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//服务端地址设置为本地回环地址
    servAddr.sin_port = htons(12345);//host to net short 端口号设置为12345

    //4、绑定服务端的socket和打包好的地址
    bind(servSock, (SOCKADDR*)&servAddr, sizeof(servAddr));

    //4.5给服务端sokect绑定一个事件对象,用来接收客户端链接的事件
    WSAEVENT servEvent = WSACreateEvent();//创建一个人工重设为传信的事件对象
    WSAEventSelect(servSock, servEvent, FD_ALL_EVENTS);//绑定事件对象,并且监听所有事件

    cliSock[0] = servSock;
    cliEvent[0] = servEvent;

    //5、开启监听
    listen(servSock, 10);//监听队列长度为10

    //6、创建接受链接的线程
    //不需要句柄所以直接关闭
    CloseHandle(CreateThread(NULL, 0, servEventThread, (LPVOID)&servSock, 0, 0));

    cout << "聊天室服务器已开启" << endl;
    connect test
    //int addrLen = sizeof(SOCKADDR);//用于接收客户端的地址包结构体长度
    //SOCKET cliSOCK = accept(servSock, (SOCKADDR*)&servAddr,&addrLen);
    //if (cliSOCK != INVALID_SOCKET) 
    //{
    //    cout << "链接成功" << endl;
    //}
    //while (1)
    //{
    //    char buf[100] = { 0 };//测试缓冲区
    //    int nrecv = recv(cliSOCK, buf, sizeof(buf), 0);
    //    if (nrecv > 0)//如果接收到客户端的信息就输出到屏幕
    //    {
    //        cout << buf << endl;
    //    }
    //}
    //需要让主线程一直运行下去
    //发送消息给全部客户端
    while (1)
    {

        char contentBuf[BUFFER_SIZE] = { 0 };
        char sendBuf[BUFFER_SIZE] = { 0 };
        cin.getline(contentBuf, sizeof(contentBuf));
        sprintf(sendBuf, "[智能小易]%s", contentBuf);
        for (int j =1 ; j <= total; j++)
        {
            send(cliSock[j], sendBuf, sizeof(sendBuf), 0);
        }

    }
    //1-关闭socket库的收尾工作
    WSACleanup();
    return 0;
}

DWORD WINAPI servEventThread(LPVOID IpParameter) //服务器端线程
{
    //该线程负责处理服务端和各个客户端发生的事件
    //将传入的参数初始化
    SOCKET servSock = *(SOCKET*)IpParameter;//LPVOID为空指针类型,需要先转成SOCKET类型再引用,即可使用传入的SOCKET
    while (1) //不停执行
    {
        for (int i = 0; i < total+1; i++)//i代表现在正在监听事件的终端
        {
            //若有一个客户端链接,total==1,循环两次,包含客户端和服务端
            //对每一个终端(客户端和服务端),查看是否发生事件,等待WAIT_TIME毫秒
            int index = WSAWaitForMultipleEvents(1, &cliEvent[i], false, WAIT_TIME, 0);

            index -= WSA_WAIT_EVENT_0;//此时index为发生事件的终端下标

            if (index==WSA_WAIT_TIMEOUT||index==WSA_WAIT_FAILED)
            {
                continue;//如果出错或者超时,即跳过此终端
            }

            else if (index==0)
            {
                WSANETWORKEVENTS networkEvents;
                WSAEnumNetworkEvents(cliSock[i], cliEvent[i], &networkEvents);//查看是什么事件

                //事件选择
                if (networkEvents.lNetworkEvents & FD_ACCEPT)//若产生accept事件(此处与位掩码相与)
                {
                    if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
                    {
                        cout <<"连接时产生错误,错误代码" << networkEvents.iErrorCode[FD_ACCEPT_BIT] << endl;
                        continue;
                    }
                    //接受链接
                    if (total + 1 < MAX_LINK_NUM)//若增加一个客户端仍然小于最大连接数,则接受该链接
                    {
                        //total为已连接客户端数量
                        int nextIndex = total + 1;//分配给新客户端的下标
                        int addrLen = sizeof(SOCKADDR);
                        SOCKET newSock = accept(servSock, (SOCKADDR*)&cliAddr[nextIndex], &addrLen);
                        if (newSock != INVALID_SOCKET)
                        {
                            //设置发送和接收时限
                            /*setsockopt(newSock, SOL_SOCKET, SO_SNDTIMEO, (const char*) & SEND_TIMEOUT, sizeof(SEND_TIMEOUT));
                            setsockopt(newSock, SOL_SOCKET, SO_SNDTIMEO, (const char*) &RECV_TIMEOUT, sizeof(RECV_TIMEOUT));*/
                            //给新客户端分配socket
                            cliSock[nextIndex] = newSock;
                            //新客户端的地址已经存在cliAddr[nextIndex]中了
                            //为新客户端绑定事件对象,同时设置监听,close,read,write
                            WSAEVENT newEvent = WSACreateEvent();
                            WSAEventSelect(cliSock[nextIndex], newEvent, FD_CLOSE | FD_READ | FD_WRITE);
                            cliEvent[nextIndex] = newEvent;
                            total++;//客户端连接数增加
                            cout <<"#" << nextIndex<< "游客(IP:" << inet_ntoa(cliAddr[nextIndex].sin_addr) << ")进入了聊天室,当前连接数:" << total << endl;

                            //给所有客户端发送欢迎消息
                            char buf[BUFFER_SIZE] = "[智能小易]欢迎游客(IP:";
                            strcat(buf, inet_ntoa(cliAddr[nextIndex].sin_addr));
                            strcat(buf, ")进入聊天室");
                            for (int j = i; j <=total; j++)
                            {
                                
                                send(cliSock[j], buf, sizeof(buf),0);
                                
                            }
                        }
                    }
                    
                }
                else if (networkEvents.lNetworkEvents & FD_CLOSE)//客户端被关闭,即断开连接
                {
                    
                    //i表示已关闭的客户端下标
                    total--;
                     cout << "#" << i << "游客(IP:" << inet_ntoa(cliAddr[i].sin_addr) << ")退出了聊天室,当前连接数:"<<total << endl;
                    //释放这个客户端的资源
                    closesocket(cliSock[i]);
                    WSACloseEvent(cliEvent[i]);

                    //数组调整,用顺序表删除元素
                    for (int j = i; j < total; j++)
                    {
                        cliSock[j] = cliSock[j + 1];
                        cliEvent[j] = cliEvent[j + 1];
                        cliAddr[j] = cliAddr[j + 1];
                    }
                    //给所有客户端发送退出聊天室的消息
                    char buf[BUFFER_SIZE] = "[智能小易](IP:";
                    strcat(buf, inet_ntoa(cliAddr[i].sin_addr));
                    strcat(buf, ")退出聊天室");
                    for (int j = 1; j <=total; j++)
                    {
                        send(cliSock[j], buf, sizeof(buf), 0);

                    }
                    
                    
                }
                else if (networkEvents.lNetworkEvents & FD_READ)//接收到消息
                {

                    char buffer[BUFFER_SIZE] = { 0 };//字符缓冲区,用于接收和发送消息
                    char buffer2[BUFFER_SIZE] = { 0 };

                    for (int j = 1; j <= total; j++)
                    {
                        int nrecv = recv(cliSock[j], buffer, sizeof(buffer), 0);//nrecv是接收到的字节数
                        if (nrecv > 0)//如果接收到的字符数大于0
                        {    
                            sprintf(buffer2,"[#%d]%s",j,buffer);
                            //在服务端显示
                            cout << buffer2 << endl;
                            //在其他客户端显示(广播给其他客户端)
                            for (int k = 1; k <= total; k++)
                            {
                                send(cliSock[k], buffer2, sizeof(buffer),0);
                            }
                        }
                        
                    }

                    
                    
                }
            }
        }

    
    }
    return 0;
}

客户端:

// 聊天程序客户端

#include"pch.h"//预编译头
#include<iostream>
#include<Winsock2.h>//socket头文件
#include<cstring>

using namespace std;

//载入系统提供的socket动态链接库

#pragma comment(lib,"ws2_32.lib")   //socket库

const int BUFFER_SIZE = 1024;//缓冲区大小

DWORD WINAPI recvMsgThread(LPVOID IpParameter);

int main() {
    //1、初始化socket库
    WSADATA wsaData;//获取版本信息,说明要使用的版本
    WSAStartup(MAKEWORD(2, 2), &wsaData);//MAKEWORD(主版本号, 副版本号)

    //2、创建socket
    SOCKET cliSock = socket(AF_INET, SOCK_STREAM, 0);//面向网路的流式套接字,第三个参数代表自动选择协议

    //3、打包地址
    //客户端
    SOCKADDR_IN cliAddr = { 0 };
    cliAddr.sin_family = AF_INET;
    cliAddr.sin_addr.s_addr = inet_addr("127.0.0.1");//IP地址
    cliAddr.sin_port = htons(12345);//端口号
    //服务端
    SOCKADDR_IN servAddr = { 0 };
    servAddr.sin_family = AF_INET;//和服务器的socket一样,sin_family表示协议簇,一般用AF_INET表示TCP/IP协议。
    servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//服务端地址设置为本地回环地址
    servAddr.sin_port = htons(12345);//host to net short 端口号设置为12345

    if (connect(cliSock, (SOCKADDR*)&servAddr, sizeof(SOCKADDR)) == SOCKET_ERROR) 
    {
        cout << "链接出现错误,错误代码" << WSAGetLastError() << endl;
    }

    //创建接受消息线程
    CloseHandle(CreateThread(NULL, 0, recvMsgThread, (LPVOID)&cliSock, 0, 0));
    //主线程用于输入要发送的消息
    while (1) 
    {
        char buf[BUFFER_SIZE] = { 0 };
        cin.getline(buf,sizeof(buf));
        if (strcmp(buf, "quit") == 0)//若输入“quit”,则退出聊天室
        {
            break;
        }
        send(cliSock, buf, sizeof(buf), 0);
    }
    closesocket(cliSock);
    WSACleanup();
    return 0;
}

DWORD WINAPI recvMsgThread(LPVOID IpParameter)//接收消息的线程
{
    SOCKET cliSock = *(SOCKET*)IpParameter;//获取客户端的SOCKET参数

    while (1)
    {
        char buffer[BUFFER_SIZE] = { 0 };//字符缓冲区,用于接收和发送消息
        int nrecv = recv(cliSock, buffer, sizeof(buffer), 0);//nrecv是接收到的字节数
        if (nrecv > 0)//如果接收到的字符数大于0
        {
            cout << buffer << endl;
        }
        else if (nrecv<0)//如果接收到的字符数小于0就说明断开连接
        {
            cout << "与服务器断开连接" << endl;
            break;
        }
    }
    return 0;
}

五、实验效果:

这里需要注意,如果想要多开客户端,需要到你的代码文件目录列表中去找到exe文件启动,开客户端就可以了

That's all.

谢谢友友们的观看,如果对你有帮助,就留个赞吧。

标签: 网络 c++ tcp/ip

本文转载自: https://blog.csdn.net/m0_48660921/article/details/122382490
版权归原作者 爱编程的小云 所有, 如有侵权,请联系我们删除。

“C++语言实现网络聊天程序(基于TCP/IP协议的SOCKET编程)超详细(代码+解析)”的评论:

还没有评论