欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

socket多人聊天程序C语言版(二)

程序员文章站 2022-11-05 11:38:52
1v1实现了,1v多也就容易了。不过相对于1v1的程序,我经过大改,采用链表来动态管理。这样效率真的提升不少,至少cpu使用率稳稳的在20以下,不会飙到100了。用c语言写这个还是挺费时间的,因为什...

1v1实现了,1v多也就容易了。不过相对于1v1的程序,我经过大改,采用链表来动态管理。这样效率真的提升不少,至少cpu使用率稳稳的在20以下,不会飙到100了。用c语言写这个还是挺费时间的,因为什么功能函数都要自己写,不像c++有stl库可以用,mfc写就更简单了,接下来我还会更新mfc版本的多人聊天程序。好了,废话少说,进入主题。

这个程序要解决的问题如下:
1.cpu使用率飙升问题 –>用链表动态管理

2.用户自定义聊天,就是想跟谁聊跟谁聊 –> _client结构体中新增一个chatname字段,用来表示要和谁聊天,这个字段很重要,因为server转发消息的时候就是按照这个字段来转发的。

3.中途换人聊天,就是聊着聊着,想和别人聊,而且自己还一样能接收到其它人发的消息 –> 这个就要小改客户端的代码了,可以在发送聊天消息之前插入一段代码,用来切换聊天用户。具体做法就是,用getch()函数读取esc键,如果用户按了这个键,则表示想切换用户,然后会输出一行提示,请输入chat name,就是想要和谁聊天的名字,发送这个名字过去之前要加一个标识符,表示这个消息是切换聊天用户消息。然后server接收到这个消息后会判断第一个字符是不是标识符,第二个字符不能是标识符,则根据这个name来查找当前在线的用户,然后修改想切换聊天用户的chatname为name这个用户。(可能有点绕,不懂的看代码就清晰易懂了~)

4.下线后提醒对方 –> 还是老套路,只要send对方不通就当对方下线了。

编写环境:win10,vs2015
效果图:
为了方便就不用演示了,但是在虚拟机是肯定可以的,应该说只要是局域网,能互相ping通就可以使用这个程序。

socket多人聊天程序C语言版(二)

socket多人聊天程序C语言版(二)

socket多人聊天程序C语言版(二)

server code:

链表头文件:

#ifndef _client_link_list_h_
#define _client_link_list_h_

#include 

#include 

//客户端信息结构体
typedef struct _client
{
    socket sclient;         //客户端套接字
    char buf[128];          //数据缓冲区
    char username[16];      //客户端用户名
    char ip[20];            //客户端ip
    unsigned short port;    //客户端端口
    uint_ptr flag;          //标记客户端,用来区分不同的客户端
    char chatname[16];      //指定要和哪个客户端聊天
    _client* next;          //指向下一个结点
}client, *pclient;

/*
* function  初始化链表
* return    无返回值
*/
void init();

/*
* function  获取头节点
* return    返回头节点
*/
pclient getheadnode();

/*
* function  添加一个客户端
* param     client表示一个客户端对象
* return    无返回值
*/
void addclient(pclient client);

/*
* function  删除一个客户端
* param     flag标识一个客户端对象
* return    返回true表示删除成功,false表示失败
*/
bool removeclient(uint_ptr flag);

/*
* function  根据name查找指定客户端
* param     name是指定客户端的用户名
* return    返回一个client表示查找成功,返回invalid_socket表示无此用户
*/
socket findclient(char* name);

/*
* function  根据socket查找指定客户端
* param     client是指定客户端的套接字
* return    返回一个pclient表示查找成功,返回null表示无此用户
*/
pclient findclient(socket client);

/*
* function  计算客户端连接数
* param     client表示一个客户端对象
* return    返回连接数
*/
int countcon();

/*
* function  清空链表
* return    无返回值
*/
void clearclient();

/*
* function  检查连接状态并关闭一个连接
* return 返回值
*/
void checkconnection();

/*
* function  指定发送给哪个客户端
* param     fromname,发信人
* param     toname,   收信人
* param     data,     发送的消息
*/
void senddata(char* fromname, char* toname, char* data);


#endif //_client_link_list_h_

链表cpp文件:

#include "clientlinklist.h"

pclient head = (pclient)malloc(sizeof(_client)); //创建一个头结点

/*
* function  初始化链表
* return    无返回值
*/
void init()
{
    head->next = null;
}

/*
* function  获取头节点
* return    返回头节点
*/
pclient getheadnode()
{
    return head;
}

/*
* function  添加一个客户端
* param     client表示一个客户端对象
* return    无返回值
*/
void addclient(pclient client)
{
    client->next = head->next;  //比如:head->1->2,然后添加一个3进来后是
    head->next = client;        //3->1->2,head->3->1->2
}

/*
* function  删除一个客户端
* param     flag标识一个客户端对象
* return    返回true表示删除成功,false表示失败
*/
bool removeclient(uint_ptr flag)
{
    //从头遍历,一个个比较
    pclient pcur = head->next;//pcur指向第一个结点
    pclient ppre = head;      //ppre指向head 
    while (pcur)
    {
        // head->1->2->3->4,要删除2,则直接让1->3
        if (pcur->flag == flag)
        {
            ppre->next = pcur->next;
            closesocket(pcur->sclient);  //关闭套接字
            free(pcur);   //释放该结点
            return true;
        }
        ppre = pcur;
        pcur = pcur->next;
    }
    return false;
}

/*
* function  查找指定客户端
* param     name是指定客户端的用户名
* return    返回socket表示查找成功,返回invalid_socket表示无此用户
*/
socket findclient(char* name)
{
    //从头遍历,一个个比较
    pclient pcur = head;
    while (pcur = pcur->next)
    {
        if (strcmp(pcur->username, name) == 0)
            return pcur->sclient;
    }
    return invalid_socket;
}

/*
* function  根据socket查找指定客户端
* param     client是指定客户端的套接字
* return    返回一个pclient表示查找成功,返回null表示无此用户
*/
pclient findclient(socket client)
{
    //从头遍历,一个个比较
    pclient pcur = head;
    while (pcur = pcur->next)
    {
        if (pcur->sclient == client)
            return pcur;
    }
    return null;
}

/*
* function  计算客户端连接数
* param     client表示一个客户端对象
* return    返回连接数
*/
int countcon()
{
    int icount = 0;
    pclient pcur = head;
    while (pcur = pcur->next)
        icount++;
    return icount;
}

/*
* function  清空链表
* return    无返回值
*/
void clearclient()
{
    pclient pcur = head->next;
    pclient ppre = head;
    while (pcur)
    {
        //head->1->2->3->4,先删除1,head->2,然后free 1
        pclient p = pcur;
        ppre->next = p->next;
        free(p);
        pcur = ppre->next;
    }
}

/*
* function 检查连接状态并关闭一个连接
* return 返回值
*/
void checkconnection()
{
    pclient pclient = getheadnode();
    while (pclient = pclient->next)
    {
        if (send(pclient->sclient, "", sizeof(""), 0) == socket_error)
        {
            if (pclient->sclient != 0)
            {
                printf("disconnect from ip: %s,username: %s\n", pclient->ip, pclient->username);
                char error[128] = { 0 };   //发送下线消息给发消息的人
                sprintf(error, "the %s was downline.\n", pclient->username);
                send(findclient(pclient->chatname), error, sizeof(error), 0);
                closesocket(pclient->sclient);   //这里简单的判断:若发送消息失败,则认为连接中断(其原因有多种),关闭该套接字
                removeclient(pclient->flag);
                break;
            }
        }
    }
}

/*
* function  指定发送给哪个客户端
* param     fromname,发信人
* param     toname,   收信人
* param     data,     发送的消息
*/
void senddata(char* fromname, char* toname, char* data)
{
    socket client = findclient(toname);   //查找是否有此用户
    char error[128] = { 0 };
    int ret = 0;
    if (client != invalid_socket && strlen(data) != 0)
    {
        char buf[128] = { 0 };
        sprintf(buf, "%s: %s", fromname, data);   //添加发送消息的用户名
        ret = send(client, buf, sizeof(buf), 0);
    }
    else//发送错误消息给发消息的人
    {
        if(client == invalid_socket)
            sprintf(error, "the %s was downline.\n", toname);
        else
            sprintf(error, "send to %s message not allow empty, please try again!\n", toname);
        send(findclient(fromname), error, sizeof(error), 0);
    }
    if (ret == socket_error)//发送下线消息给发消息的人
    {
        sprintf(error, "the %s was downline.\n", toname);
        send(findclient(fromname), error, sizeof(error), 0);
    }

}

server cpp:

/*

#include 
#include 
#include 
#include "clientlinklist.h"
#pragma comment(lib,"ws2_32.lib")


socket g_serversocket = invalid_socket;      //服务端套接字
sockaddr_in g_clientaddr = { 0 };            //客户端地址
int g_iclientaddrlen = sizeof(g_clientaddr);

typedef struct _send
{
    char fromname[16];
    char toname[16];
    char data[128];
}send,*psend;




//发送数据线程
unsigned __stdcall threadsend(void* param)
{
    psend psend = (psend)param;  //转换为send类型
    senddata(psend->fromname, psend->toname, psend->data); //发送数据
    return 0;
}


//接受数据
unsigned __stdcall threadrecv(void* param)
{
    int ret = 0;
    while (1)
    {
        pclient pclient = (pclient)param;
        if (!pclient)
            return 1;
        ret = recv(pclient->sclient, pclient->buf, sizeof(pclient->buf), 0);
        if (ret == socket_error)
            return 1;
        if (pclient->buf[0] == '#' && pclient->buf[1] != '#') //#表示用户要指定另一个用户进行聊天
        {
            socket socket = findclient(&pclient->buf[1]);    //验证一下客户是否存在
            if (socket != invalid_socket)
            {
                pclient c = (pclient)malloc(sizeof(_client));
                c = findclient(socket);                        //只要改变chatname,发送消息的时候就会自动发给指定的用户了
                memset(pclient->chatname, 0, sizeof(pclient->chatname));   
                memcpy(pclient->chatname , c->username,sizeof(pclient->chatname));
            }
            else  
                send(pclient->sclient, "the user have not online or not exits.",64,0);
            continue;
        }

        psend psend = (psend)malloc(sizeof(_send));
        //把发送人的用户名和接收消息的用户和消息赋值给结构体,然后当作参数传进发送消息进程中
        memcpy(psend->fromname, pclient->username, sizeof(psend->fromname));
        memcpy(psend->toname, pclient->chatname, sizeof(psend->toname));
        memcpy(psend->data, pclient->buf, sizeof(psend->data));
        _beginthreadex(null, 0, threadsend, psend, 0, null);
        sleep(200);
    }

    return 0;
}

//开启接收消息线程
void startrecv()
{
    pclient pclient = getheadnode();
    while (pclient = pclient->next)
        _beginthreadex(null, 0, threadrecv, pclient, 0, null);
}

//管理连接
unsigned __stdcall threadmanager(void* param)
{
    while (1)
    {
        checkconnection();  //检查连接状况
        sleep(2000);        //2s检查一次
    }

    return 0;
}

//接受请求
unsigned __stdcall threadaccept(void* param)
{
    _beginthreadex(null, 0, threadmanager, null, 0, null);
    init(); //初始化一定不要再while里面做,否则head会一直为null!!!
    while (1)
    {
        //创建一个新的客户端对象
        pclient pclient = (pclient)malloc(sizeof(_client));

        //如果有客户端申请连接就接受连接
        if ((pclient->sclient = accept(g_serversocket, (sockaddr*)&g_clientaddr, &g_iclientaddrlen)) == invalid_socket)
        {
            printf("accept failed with error code: %d\n", wsagetlasterror());
            closesocket(g_serversocket);
            wsacleanup();
            return -1;
        }
        recv(pclient->sclient, pclient->username, sizeof(pclient->username), 0); //接收用户名和指定聊天对象的用户名
        recv(pclient->sclient, pclient->chatname, sizeof(pclient->chatname), 0);

        memcpy(pclient->ip, inet_ntoa(g_clientaddr.sin_addr), sizeof(pclient->ip)); //记录客户端ip
        pclient->flag = pclient->sclient; //不同的socke有不同uint_ptr类型的数字来标识
        pclient->port = htons(g_clientaddr.sin_port);
        addclient(pclient);  //把新的客户端加入链表中

        printf("successfuuly got a connection from ip:%s ,port: %d,uername: %s , chatname: %s\n",
            pclient->ip, pclient->port, pclient->username,pclient->chatname);

        if (countcon() >= 2)                     //当至少两个用户都连接上服务器后才进行消息转发                                                          
            startrecv();

        sleep(2000);
    }
    return 0;
}

//启动服务器
int startserver()
{
    //存放套接字信息的结构
    wsadata wsadata = { 0 };
    sockaddr_in serveraddr = { 0 };             //服务端地址
    ushort uport = 18000;                       //服务器监听端口

    //初始化套接字
    if (wsastartup(makeword(2, 2), &wsadata))
    {
        printf("wsastartup failed with error code: %d\n", wsagetlasterror());
        return -1;
    }
    //判断版本
    if (lobyte(wsadata.wversion) != 2 || hibyte(wsadata.wversion) != 2)
    {
        printf("wversion was not 2.2\n");
        return -1;
    }
    //创建套接字
    g_serversocket = socket(af_inet, sock_stream, ipproto_tcp);
    if (g_serversocket == invalid_socket)
    {
        printf("socket failed with error code: %d\n", wsagetlasterror());
        return -1;
    }

    //设置服务器地址
    serveraddr.sin_family = af_inet;//连接方式
    serveraddr.sin_port = htons(uport);//服务器监听端口
    serveraddr.sin_addr.s_un.s_addr = htonl(inaddr_any);//任何客户端都能连接这个服务器

    //绑定服务器
    if (socket_error == bind(g_serversocket, (sockaddr*)&serveraddr, sizeof(serveraddr)))
    {
        printf("bind failed with error code: %d\n", wsagetlasterror());
        closesocket(g_serversocket);
        return -1;
    }
    //设置监听客户端连接数
    if (socket_error == listen(g_serversocket, 20000))
    {
        printf("listen failed with error code: %d\n", wsagetlasterror());
        closesocket(g_serversocket);
        wsacleanup();
        return -1;
    }

    _beginthreadex(null, 0, threadaccept, null, 0, 0);
    for (int k = 0;k < 100;k++) //让主线程休眠,不让它关闭tcp连接.
        sleep(10000000);

    //关闭套接字
    clearclient();
    closesocket(g_serversocket);
    wsacleanup();
    return 0;
}

int main()
{
    startserver(); //启动服务器

    return 0;
}

client code:

#define _winsock_deprecated_no_warnings
#include 
#include 
#include 
#include 
#include 
#pragma comment(lib,"ws2_32.lib")
#define recv_over 1
#define recv_yet 0
char username[16] = { 0 };
char chatname[16] = { 0 };
int istatus = recv_yet;
//接受数据
unsigned __stdcall threadrecv(void* param)
{
    char buf[128] = { 0 };
    while (1)
    {
        int ret = recv(*(socket*)param, buf, sizeof(buf), 0);
        if (ret == socket_error)
        {
            sleep(500);
            continue;
        }
        if (strlen(buf) != 0)
        {
            printf("%s\n", buf);
            istatus = recv_over;
        }
        else
            sleep(100);   


    }
    return 0;
}

//发送数据
unsigned __stdcall threadsend(void* param)
{
    char buf[128] = { 0 };
    int ret = 0;
    while (1)
    {
        int c = getch();
        if (c == 27)   //esc ascii是27
        {
            memset(buf, 0, sizeof(buf));
            printf("please input the chat name:");
            gets_s(buf);
            char b[17] = { 0 };
            sprintf(b, "#%s", buf);
            ret = send(*(socket*)param,b , sizeof(b), 0);
            if (ret == socket_error)
                return 1;
            continue;
        }
        if(c == 72 || c == 0 || c == 68)//为了显示美观,加一个无回显的读取字符函数
            continue;                   //getch返回值我是经过实验得出如果是返回这几个值,则getch就会自动跳过,具体我也不懂。
        printf("%s: ", username);
        gets_s(buf);
        ret = send(*(socket*)param, buf, sizeof(buf), 0);
        if (ret == socket_error)
            return 1;
    }
    return 0;
}



//连接服务器
int connectserver()
{
    wsadata wsadata = { 0 };//存放套接字信息
    socket clientsocket = invalid_socket;//客户端套接字
    sockaddr_in serveraddr = { 0 };//服务端地址
    ushort uport = 18000;//服务端端口
    //初始化套接字
    if (wsastartup(makeword(2, 2), &wsadata))
    {
        printf("wsastartup failed with error code: %d\n", wsagetlasterror());
        return -1;
    }
    //判断套接字版本
    if (lobyte(wsadata.wversion) != 2 || hibyte(wsadata.wversion) != 2)
    {
        printf("wversion was not 2.2\n");
        return -1;
    }
    //创建套接字
    clientsocket = socket(af_inet, sock_stream, ipproto_tcp);
    if (clientsocket == invalid_socket)
    {
        printf("socket failed with error code: %d\n", wsagetlasterror());
        return -1;
    }
    //输入服务器ip
    printf("please input server ip:");
    char ip[32] = { 0 };
    gets_s(ip);
    //设置服务器地址
    serveraddr.sin_family = af_inet;
    serveraddr.sin_port = htons(uport);//服务器端口
    serveraddr.sin_addr.s_un.s_addr = inet_addr(ip);//服务器地址

    printf("connecting......\n");
    //连接服务器
    if (socket_error == connect(clientsocket, (sockaddr*)&serveraddr, sizeof(serveraddr)))
    {
        printf("connect failed with error code: %d\n", wsagetlasterror());
        closesocket(clientsocket);
        wsacleanup();
        return -1;
    }
    printf("connecting server successfully ip:%s port:%d\n",
        ip, htons(serveraddr.sin_port));
    printf("please input your username: ");
    gets_s(username);
    send(clientsocket, username, sizeof(username), 0);
    printf("please input the chatname: ");
    gets_s(chatname);
    send(clientsocket, chatname, sizeof(chatname), 0);
    printf("\n\n");

    _beginthreadex(null, 0, threadrecv, &clientsocket, 0, null); //启动接收和发送消息线程
    _beginthreadex(null, 0, threadsend, &clientsocket, 0, null);
    for (int k = 0;k < 1000;k++)
        sleep(10000000);
    closesocket(clientsocket);
    wsacleanup();
    return 0;
}

int main()
{
    connectserver(); //连接服务器
    return 0;
}

最后,需要改进的有以下几点:
1.没有消息记录,所以最好用文件或者的方式记录,个人推荐数据库。

2.没有用户注册,登陆的操作,也是用文件或者数据库来弄。程序一运行就读取数据库信息就行。

3.群聊功能没有弄,这个其实很简单,就是服务器不管3721,把接收到的消息转发给所有在线用户。

4.没有离线消息,这个就用数据库存储离线消息,然后用户上线后立即发送过去就行。

最后总结一下,没有数据库的聊天程序果然功能简陋~,c语言写的程序要注意对内存的操作。还有tcp方式的连接太费时费内存(用户量达的时候)。