开源项目:TCP模拟实现QQ群聊(云服务器版)

基于多线程TCP协议模拟实现群聊(云服务器版)

1.思路

①使用链表保存每一个accept返回的socket文件描述符,在服务端给客户端send数据的时候,循环遍历链表,给每个socket文件描述符都发送,从而即可实现简单的群聊
②再将自己的云服务器的外网和内网IP找到,在服务器代码中绑定内网IP,在客户端中绑定外网IP,然后再固定一个开放的端口号(自己设置,一般设置1024之后,如果不自己设置开放的端口号,所有的端口号都可以使用,但是这样是不安全的)

2.云服务器如何开放端口号(以腾讯云为例)

①登录并打开云主机页面,红线标记处就是外网IP和内网IP
这里写图片描述
②选择“安全组”,点击新建
这里写图片描述
③有三种选项,模板选择图示中的即可,名称随便改
这里写图片描述
④点击新建好的安全组,进入图示界面,点击添加规则
这里写图片描述
⑤按照图示填写即可,端口号最好大于1024
这里写图片描述

3.碰到的问题

有可能在这里有你写群聊的过程中遇到的困惑的小问题,看看能不能帮助你解决

①为什么一个客户端发送消息后,另一个客户端不能立刻收到,而是得自己发送一次后才能看到上一个人发送的消息?
答:看看是不是客户端中的send和recv设置成阻塞式的了。
②为什么将send和recv都设置成非阻塞的后,还是出现上一个问题?
答:如果是用read从标准输入读取键入内容,如read(0,buf,sizeof(buf));,那么就使用fcntl函数将0号文件,即标准输入,更改成非阻塞的。
③为什么会出现发送多条消息后服务器就崩了的情况,只有一个客户端连接的时候还好,有两个或以上的客户端就会这样?
答:根据我某个叫小刚的朋友的经历,出现这个问题就是链表没写好,请仔细检查链表的插入和删除,以及空间的释放。

3.代码

代码分成三部分:
protocol.h包含了各种自定义协议(即一些结构体和宏)
client.c是客户端代码
pthread_server.c是服务器代码
注:所有代码都是在通过xshell链接云服务器编写,即代码存储在云服务器中,读者可以自行将服务器写成守护进程,这样只要别人运行你的客户端程序,即可实现群聊

头文件

#ifndef __PROTOCOL_H__
#define __PROTOCOL_H__

#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>

#define NAME 10 //用户名长度
#define MSG 1024 //发送信息大小
#define IP "172.21.0.4" //外网IP
#define PIP "192.144.188.26" //内网IP
#define PORT 2020 //固定的开放的端口号 

typedef struct SocketListNode
{
    int _sockfd;
    struct SocketListNode* _prev;
    struct SocketListNode* _next;
}SLN;//带头双向链表,存储所有客户端套接文件描述符

typedef struct User
{
    char _name[NAME];//用户名
    char _msg[MSG];//用户发送的信息
}user;

#endif //__PROTOCOL_H___

客户端

#include "protocol.h"

int ClientDo(char* argv[])//客户端工作前的准备工作
{
   int fd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
   if(fd<0)
   {
       perror("socket");
       exit(2);
   }

    //填充服务器的套接字信息
   struct sockaddr_in server;
   server.sin_family=AF_INET;
   server.sin_port=htons(PORT);
   server.sin_addr.s_addr=inet_addr(PIP);
   if(connect(fd,(struct sockaddr*)&server,sizeof(server))<0)//建立连接
   {
       perror("connect");
       exit(3);
   }  
   return fd;
}

void working(int sockfd,char* name)//客户端工作ing
{
    if(fcntl(0,F_SETFL,FNDELAY)<0)//将0号文件设置成非阻塞的
    {
        perror("fcntl");
        exit(4);
    }
    user u;
    char buf[NAME+MSG]={0};//存储序列化后的数据

    while(1)
    {
        strncpy(buf,name,NAME);

        ssize_t r=read(0,u._msg,MSG);//从标准输入读取键入内容
        if(r>0)
        {
            u._msg[r]=0;
            fflush(stdout);
            strcpy(buf+NAME,u._msg);
            send(sockfd,buf,NAME+MSG,MSG_DONTWAIT);//非阻塞式发送
        }
        ssize_t rec=recv(sockfd,buf,NAME+MSG,MSG_DONTWAIT);//非阻塞式接收
        if(rec>0)
        {
            strncpy(u._name,buf,NAME);
            strncpy(u._msg,buf+NAME,MSG);
            printf("%s :>%s",u._name,u._msg);
        }
    }
    close(sockfd);
}

int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        printf("parameter is too less\n");
        return 1;
    }

    int sockfd=ClientDo(argv);

    working(sockfd,argv[1]);

    return 0;
}

服务端

#include "protocol.h"

SLN* BuyNode(int sockfd)//创建节点
{
    SLN* node=(SLN*)malloc(sizeof(SLN));
    if(node==NULL)
    {
        perror("malloc");
        exit(1);
    }
    node->_sockfd=sockfd;
    node->_prev=NULL;
    node->_next=NULL;
    return node;
}

SLN* phead=NULL;//全局的头节点,方便操作

int ServerDo()//服务器工作前的准备工作
{
    int fd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
    if(fd<0)
    {
        perror("socket");
        exit(2);
    }

    //填充自己的套接字信息
    struct sockaddr_in server;
    server.sin_family=AF_INET;
    server.sin_port=htons(PORT);
    server.sin_addr.s_addr=inet_addr(IP);

    if(bind(fd,(struct sockaddr*)&server,sizeof(server))<0)//绑定端口号
    {
        perror("bind");
        exit(3);
    }
    if(listen(fd,5)<0)//将套接字设置为监听状态
    {
        perror("listen");
        exit(4);
    }
    return fd;
}

void func(void* arg)//线程函数
{
    SLN* node=(SLN*)arg;
    SLN* cur=NULL;
    char buf[NAME+MSG]={0};
    while(1)
    {
        ssize_t rec=recv(node->_sockfd,buf,NAME+MSG,0);//阻塞式接收
        if(rec<=0)
        {
            printf("sockfd closed\n");
            break;
        }
        cur=phead->_next;
        while(cur!=NULL)//循环给每个套接字都发送
        {
            send(cur->_sockfd,buf,NAME+MSG,MSG_DONTWAIT);
            cur=cur->_next;
        }
    }

    //删除节点,释放空间
    node->_prev->_next=node->_next;
    if(node->_next!=NULL)
    {
        node->_next->_prev=node->_prev;
    }
    node->_prev=NULL;
    node->_next=NULL;
    free(node);
    close(node->_sockfd);
}

void working(int sockfd)//服务器工作ing
{
    int new_sockfd=0;
    while(1)
    {
        new_sockfd=accept(sockfd,NULL,NULL);//接收链接
        if(new_sockfd<0)
        {
            perror("accept");
            continue;
        }

        //头插节点,保存客户端套接字
        SLN* node=BuyNode(new_sockfd);
        node->_next=phead->_next;
        node->_prev=phead;
        if(phead->_next!=NULL)
        {
            phead->_next->_prev=node;
        }
        phead->_next=node;

        pthread_t thread=0;
        pthread_create(&thread,NULL,(void*)func,(void*)node);
        pthread_detach(thread);//将线程设置成分离态,让其“自生自灭”
    }
    close(sockfd);
}

int main()
{
    phead=BuyNode(0);//初始化链表头节点

    int sockfd=ServerDo();

    working(sockfd);

    return 0;
}

运行结果图

这里写图片描述

猜你喜欢

转载自blog.csdn.net/w_y_x_y/article/details/80851483