本文采用的是 多线程的方式实现信息的接收和广播以及输入的等待,之前采用的 多进程方案后来由于 僵尸进程过多会导致系统的处理压力越来越大。我没有想到对于多进程下更好的解决方案,所以使用多线程来解决这个问题,这样一来很好的避免了僵尸进程的问题,如果大家有更好的多进程处理方法也欢迎和我交流!

OSI和TCP/IP:

OSI 模型本身不是网络体系结构的全部内容,它并未确切地描述用于各层的协议和服务,仅提出每一层应该做什么。不过OSI 已经为各层制定了标准,但并不是参考模型的一部分,而作为单独的国际标准公布的。
TCP/IP 是一组用于实现网络互连的通信协议。Internet 网络体系结构以TCP/IP 为核心。基于TCP/IP 的参考模型将协议分成四个层次,它们分别是:网络访问层、网际互联层、传输层(主机到主机)、和应用层。
OSI模型和TCP/IP模型的对应关系

关于TCP/IP协议

TCP/IP是一个网络通信模型,以及一整个网络传输协议家族,是网际网络的基础通信架构。因为该协议家族的两个核心协议:TCP(传输控制协议)和IP(网际协议),为该家族中最早通过的标准。故此它常被通称为TCP/IP协议族(TCP/IP Protocol Suite),简称TCP/IP。

. TCP/IP模型 :TCP/IP 是一组用于实现网络互连的通信协议。Internet 网络体系结构以 TCP/IP 为核心。基于TCP/IP 的参考模型将协议分成四个层次,它们分别是:网络访问层、网际互联层、传输层(主机到主机)、和应用层。

  1. 应用层
    应用层对应于 OSI 参考模型的高层,为用户提供所需要的各种服务,例如:FTP、Telnet、DNS、SMTP 等。
  2. 传输层
    传输层对应于 OSI 参考模型的传输层,为应用层实体提供端到端的通信功能,保证了数据包的顺序传送及数据的完整性。该层定义了两个主要的协议:传输控制协议(TCP)和用户数据协议(UDP)。TCP 协议提供的是一种可靠的、通过“三次握手”来连接的数据传输服务;而 UDP 协议提供的则是不保证可靠的(并不是不可靠)、无连接的数据传输服务.
  3. 网际互联层网际互联层对应于 OSI 参考模型的网络层,主要解决主机到主机的通信问题。它所包含的协议设计数据包在整个网络上的逻辑传输。注重重新赋予主机一个 IP 地址来完成对主机的寻址,它还负责数据包在多种网络中的路由。该层有三个主要协议:网际协议(IP)、互联网组管理协议(IGMP)和互联网控制报文协议(ICMP)。IP 协议是网际互联层最重要的协议,它提供的是一个可靠、无连接的数据报传递服务。
  4. 网络接口层(即主机-网络层)
    网络接入层与 OSI 参考模型中的物理层和数据链路层相对应。它负责监视数据在主机和网络之间的交换。事实上,TCP/IP 本身并未定义该层的协议,而由参与互连的各网络使用自己的物理层和数据链路层协议,然后与 TCP/IP 的网络接入层进行连接。地址解析协议(ARP)工作在此层,即 OSI 参考模型的数据链路层;

. IP协议 :IP(Internet Protocol)协议是 TCP/IP 的核心协议。IP 协议(Internet Protocol)又称互联网协议,是支持网间互连的数据报协议。它提供网间连接的完善功能, 包括 IP 数据报规定互连网络范围内的 IP 地址格式。
目前的 IP 地址(IPv4:IP 第 4 版本)由 32 个二进制位表示,每 8 位二进制数为一个整数,中间由小数点间隔,如 159.226.41.98,整个 IP 地址空间有 4 组 8 位二进制数,由表示主机所在的网络的地址以及主机在该网络中的标识共同组成,它通常被分为A,B,C,D,E五类,其中商业应用只用到A,B,C三类1
在这里插入图片描述

关于UDP协议

UDP 协议(用户数据报协议)是建立在 IP 协议基础之上的,用在传输层的协议。UDP 提供了无连接的数据报服务。UDP 和 IP 协议一样,是不可靠的数据报服务。

  1. UDP 提供无连接服务
  2. UDP 缺乏可靠性支持,应用程序必须实现:确认、超时、重传、流控等
  3. UDP 面向记录服务

UDP编程的一般步骤2

  1. UDP编程的服务器端一般步骤是:
      1、创建一个socket,用函数socket();
      2、设置socket属性,用函数setsockopt();* 可选
      3、绑定IP地址、端口等信息到socket上,用函数bind();
      4、循环接收数据,用函数recvfrom();
      5、关闭网络连接;

  2. UDP编程的客户端一般步骤是:
      1、创建一个socket,用函数socket();
      2、设置socket属性,用函数setsockopt();* 可选
      3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
      4、设置对方的IP地址和端口等属性;
      5、发送数据,用函数sendto();
      6、关闭网络连接;

Socket套接字网络编程

在一般的网络编程中,我们一般不会直接去操作TCP/IP协议的底层,而是用过socket套接字进行编程,他相当于一个TCP/IP与应用程序的中间层,通过套接字中提供的接口函数我们可以很快的上手网络开发。
接下来我们来了解一下socket中的一些常用函数:
. socket()函数

int socket(int family, int type, int protocol)
  1. family指明了协议族/域,通常AF_INET、AF_INET6、AF_LOCAL等;
  2. type是套接口类型,主要SOCK_STREAM、SOCK_DGRAM、SOCK_RAW;
  3. protocol一般取为0。成功时,返回一个小的非负整数值,与文件描述符类似。
  4. 返回值:非负描述符 – 成功,-1 - 出错

. bind()函数

int bind(int sockfd,const struct sockaddr* myaddr,socklen_t addrlen)
  1. sockfd是socket函数返回的描述符;
  2. myaddr指定了想要绑定的IP和端口号,均要使用网络字节序-即大端模式;
  3. addrlen是前面struct sockaddr(与sockaddr_in等价)的长度。
  4. 返回值:0 – 成功,-1 - 出错

. listen()函数

int listen(int sockfd, int backlog)
  1. sockfd是socket函数返回的描述符;
  2. backlog这个参数涉及到一些网络的细节。在进程正理一个一个连接请求的时候,可能还存在其它的连接请求。因为TCP连接是一个过程,所以可能存在一种半连接的状态,有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。如果这个情况出现了,服务器进程希望内核如何处理呢?内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理或正在进行的连接,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。毫无疑问,服务器进程不能随便指定一个数值,内核有一个许可的范围。这个范围是实现相关的。很难有某种统一,一般这个值会小30以内。
  3. 返回值:0 – 成功,-1 - 出错

. accept()函数

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);   
  1. sockfd, 利用系统调用socket()建立的套接字描述符,通过bind()绑定到一个本地地址(一般为服务器的套接字),并且通过listen()一直在监听连接;
  2. addr, 指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写,返回地址addr的确切格式由套接字的地址类别(比如TCP或UDP)决定;若addr为NULL,没有有效地址填写,这种情况下,addrlen也不使用,应该置为NULL;
  3. addrlen, 一个值结果参数,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值;
  4. 返回值:非负数– 成功,-1 - 出错.

. connect()函数

int connect(int sockfd,conststruct sockaddr *addr, socklen_t addrlen)    
  1. 通过此函数建立于TCP服务器的连接,实际是发起三次握手过程,仅在连接成功或失败后返回。参数sockfd是本地描述符,addr为服务器地址,addrlen是socket地址长度。
  2. UDP的connect函数,结果与tcp调用不相同,没有三次握手过程。内核只是记录对方的ip和端口号,他们包含在传递给connect的套接口地址结构中,并立即返回给调用进程。
  3. 返回值:非负数– 成功,-1 - 出错

. send()函数

int send( SOCKET s, const char FAR *buf, int len, int flags );
  1. 该函数的第一个参数指定发送端套接字描述符;
  2. 第二个参数指明一个存放应用程序要发送数据的缓冲区;
  3. 第三个参数指明实际要发送的数据的字节数;
  4. 第四个参数一般置0。

. recv ()函数

int recv( SOCKET s, char FAR *buf, int len, int flags);
  1. 该函数的第一个参数指定发送端套接字描述符;
  2. 第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
  3. 第三个参数指明buf的长度;
  4. 第四个参数一般置0。

. sendto ()函数

int sendto( SOCKET s, const char FAR* buf, int len, int flags, const struct sockaddr FAR* to, int tolen)
  1. s:一个标识套接口的描述字。
  2. buf:包含待发送数据的缓冲区。
  3. len:buf缓冲区中数据的长度。
  4. flags:调用方式标志位。
  5. to:(可选)指针,指向目的套接口的地址。
  6. tolen:to所指地址的长度。

. recvfrom ()函数

int recvfrom( SOCKET s, char FAR* buf, int len, int flags)
  1. s:标识一个已连接套接口的描述字。
  2. buf:接收数据缓冲区。
  3. len:缓冲区长度。
  4. flags:调用操作方式。
  5. from:(可选)指针,指向装有源地址的缓冲区。
  6. fromlen:(可选)指针,指向from缓冲区长度值。

UDP网络聊天室实现

功能概述

利用socket编程基础实现一个基础的聊天室功能,对新成员的加入进行广播,同时具有群发和私聊两种聊天模式。

  1. 服务器端采用单线程工作模式,这种情况仅适合单一聊天室的数据服务。若要开启多个聊天室,可以使用多线程。
  2. 客户端 采用多线程编程模式,一个线程进行消息的接受,另一个线程实现用户输入和消息的发送。

服务器编程

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

#define MYPORT 8887
 
#define ERR_EXIT(m) \
    do { \
    perror(m); \
    exit(EXIT_FAILURE); \
    } while (0)
 
void echo_ser(int sock)
{
    
}
 
int main(void)
{
    int sock,portl,numb;
    char addrl[2048];
    if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
        ERR_EXIT("socket error");
    
    struct sockaddr_in servaddr,peeraddr,peeaddr;
    memset(&servaddr, 0, sizeof(servaddr));//初始化清空数组
    memset(&peeaddr, 0, sizeof(peeaddr));
    char ipstr[128];
    /*初始化套接字结构体*/
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(MYPORT);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    peeaddr.sin_family = AF_INET;
    
    printf("服务器启动\n",MYPORT);
    if (bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind error");
    
    char recvbuf[2048] = {0};
    char addr[30][100];//用户ip列表
    
    int  port[30];//端口号数组
    char name[30][100];//用户名数组
   
    char *ip;
    socklen_t peerlen;
    int n,nu=0,lon,in=0,xx,check,chec;
    while (1)
    {
        
        peerlen = sizeof(peeraddr);
        memset(recvbuf, 0, sizeof(recvbuf));
        n = recvfrom(sock, recvbuf, sizeof(recvbuf), 0,(struct sockaddr *)&peeraddr, &peerlen);46.//通篇程序唯一的接收函数。所有的刷新数据全部来自这里
        if(n!=0){
        printf("%s %d %s\n",ip=inet_ntop(AF_INET,(struct sockaddr *)&peeraddr.sin_addr.s_addr,ipstr,sizeof(ipstr)),portl=ntohs(peeraddr.sin_port),recvbuf);
 /*下面这部分用来做新用户进入控制,如果用户IP地址已经存在,
 说明此用户是中途退出后再进入的,就不在对用户数据进行添加,仅仅修改端口号就可以;
 如果ip地址是新的IP,那么创建新的数据列。存储新的连接信息。*/
        if (recvbuf[0]=='>'){
            if(nu==0) in=1;
            else{
            for(check=0;check<nu;check++){ //检测IP是否重复
                in=0;
                for(int mm=0;mm<13;mm++){
                  if(ip[mm]!=addr[check][mm])
                  {
                  in=1; 
                  } 
                  }
                if(in==0)break;   //如果IP地址重复,立即跳出循环,此时的check的值就是重复的IP数组号        
            }
            }
            //printf("in %d\n",in);
            if(in==1){
             for(lon=0;lon<peerlen+1;lon++){
                   addr[nu][lon]=ip[lon];
                   name[nu][lon]=recvbuf[lon];
             }
            port[nu]=portl;
             
            // name[nu]=recvbuf;
             xx=nu;
             nu++;
             
            }
            else{
               port[check]=portl;
               xx=check;
            }
             printf("%s进入房间,IP:%s\n",name[xx],addr[xx]);
             for(int fnu=0;fnu<nu;fnu++){//向所有在线用户发送新用户进入的信息 
                 peeaddr.sin_addr.s_addr = inet_addr(addr[fnu]);
                 peeaddr.sin_port = htons(port[fnu]);
                 printf("发送欢迎信息给: %s   %d\n",addr[fnu],port[fnu]);
                 sendto(sock, recvbuf, 20, 0,(struct sockaddr *)&peeaddr, sizeof(peeaddr));
             }
             memset(recvbuf, 0, sizeof(recvbuf));
        }
        else if(n > 0)
        {//如果接受的数据前面不包含>这说明不是新用户入列信息.
            
            printf("接收到的数据:%s\n",recvbuf);
 /*这一部分代码针对私聊信息进行处理,若果接受的信息包含@字符,这说明这是私聊*/
            if(recvbuf[0]=='@')
            {
                
                int mn;
                for(mn=0;mn<100;mn++){//获取私聊对象用户名
                    if(recvbuf[mn]=='^') break;
                }
                
                for(numb=0;numb<nu;numb++)
                {//遍历在线的用户名数组,如果匹配成功,就可以获得这个用户的数组号,从而得到他的全部连接信息
                    int ig=1;
                    for(int ii=1;ii<mn;ii++){
                     
                        if(recvbuf[ii]!=name[numb][ii]){
                            ig=0;
                            break;
                        }
                        
                    }
                    if(ig==1) break;
                    
                }
                /*匹配成功,配置接收方socke信息*/
                peeaddr.sin_addr.s_addr = inet_addr(addr[numb]);
                peeaddr.sin_port = htons(port[numb]);
                for(chec=0;chec<nu;chec++){
                in=0;
                for(int mm=0;mm<13;mm++){
                  if(ip[mm]!=addr[chec][mm])
                  {
                  in=1;
                  }
                  }
                  if(in==0)break;          
                }

               
                printf("mn:%d chec %d\n",mn,chec);
                memset(addrl, 0, sizeof(addrl));
                for(int ad=0;ad<mn;ad++)
                {
                    addrl[ad]=name[chec][ad+1];
                }
                 strcat (addrl,":");
                 
                 printf("%s发送信息给:%s\n",addrl,addr[numb]);
                 strcat (addrl,recvbuf);
                 sendto(sock, addrl, strlen(addrl), 0,(struct sockaddr *)&peeaddr, sizeof(peeaddr));
                 

            }
            else{
             /*对于群发消息,这里开始循环发送信息给所有用户*/
                for(chec=0;chec<nu;chec++){
                in=0;
                for(int mm=0;mm<13;mm++){
                  printf("%c %c\n",ip[mm],addr[chec][mm]);
                  if(ip[mm]!=addr[chec][mm])
                  {
                  
                  in=1;
                  }
                  }
                printf("%s \n",addr[chec]);
                  if(in==0)break;          
                }
                for(int tt=0;tt<nu;tt++){
                printf("%s \n",addr[tt]);
                }
                for(int ad=0;ad<strlen(name[chec]);ad++)
                {
                    addrl[ad]=name[chec][ad+1];
                }
                
                strcat (addrl,":");
                 
                printf(" %s发送信息:   %s\n",addrl,recvbuf);
                strcat (addrl,recvbuf);
                for(int fnu=0;fnu<nu;fnu++){
                     printf("发送信息给:%s\n",addr[fnu]);
                     peeaddr.sin_addr.s_addr = inet_addr(addr[fnu]);
                     peeaddr.sin_port = htons(port[fnu]);
                     sendto(sock, addrl, strlen(addrl), 0,(struct sockaddr *)&peeaddr, sizeof(peeaddr));
                     printf(" %s\n",addrl);
                     
                }             
             memset(addrl, 0, sizeof(addrl));
            }

         }
        
        }
        memset(recvbuf, 0, sizeof(recvbuf));
    }
    close(sock);
    
    return 0;
}

客户端编程

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <termios.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/time.h>
#include <pthread.h>


#define MAX 10
#define MYPORT 8887
#define user ">winds"
#define ERR_EXIT(m) \
    do \
{ \
    perror(m); \
    exit(EXIT_FAILURE); \
    } while(0)

char* SERVERIP = "192.168.1.100"; 
pthread_t thread[2];  //两个线程
pthread_mutex_t mut;
int number=0;
int i;
pthread_t id_1;
int sock;

struct sockaddr_in servaddr;
pid_t pid;
int ret;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
int flag=1;
char addr[30]={0};

 //线程一,这个线程负责循环接收信息并且显示出来。
void *thread1()
{
        while (flag)
        {
         ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
            if(recvbuf[0]=='>'){
                printf("欢迎:%s 进入房间\n",recvbuf);
            }  
            else if(recvbuf[0]!='>'){
             printf("%s\n",recvbuf);
                } 
        memset(recvbuf, 0, sizeof(recvbuf));
        memset(sendbuf, 0, sizeof(sendbuf));

        }
    
    close(sock);
        pthread_exit(NULL);
}
 //线程二,这个线程用来阻塞接收输入信息,并发送出去
void *thread2()
{
       // printf("thread2 : I'm thread 2/n");

        while (flag)
        {
                      
            printf("发送:%s\n",sendbuf);
            fgets(sendbuf, sizeof(sendbuf), stdin);
            sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));

        
        memset(recvbuf, 0, sizeof(recvbuf));
        memset(sendbuf, 0, sizeof(sendbuf));

    }
    
    close(sock);
        pthread_exit(NULL);
}

void thread_create(void)   //创建两个线程
{
        int temp;
        memset(&thread, 0, sizeof(thread));          
        /*创建线程*/
        if((temp = pthread_create(&thread[0], NULL, thread1, NULL)) != 0)     
                printf("线程1创建失败!/n");
        //else
                //printf("线程1被创建/n");

        if((temp = pthread_create(&thread[1], NULL, thread2, NULL)) != 0)  
                printf("线程2创建失败");
        //else
               // printf("线程2被创建/n");
}

void thread_wait(void)
{
        /*等待线程结束*/
        if(thread[0] !=0)

           {             //comment4    

                pthread_join(thread[0],NULL);
                printf("线程1已经结束/n");
          }
        if(thread[1] !=0) 

           {  

                //comment5

               pthread_join(thread[1],NULL);
                printf("线程2已经结束/n");
         }
}

int main()
{
        /*用默认属性初始化互斥锁*/
        pthread_mutex_init(&mut,NULL);
        printf("用户: %s\n",user);
        if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
        ERR_EXIT("socket");
        memset(&servaddr, 0, sizeof(servaddr));
           servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(MYPORT);
        servaddr.sin_addr.s_addr = inet_addr(SERVERIP);
         sendto(sock,user,8,0,(struct sockaddr *)&servaddr, sizeof(servaddr)); //发送用户信息给服务器
        thread_create();
        thread_wait();
       
        return 0;
}

运行结果

在这里插入图片描述
这是用户进入房间时,服务器的显示情况,每一次一个新用户进入房间,服务器将会把该用户的用户名向所有在线用户发送一遍,如果该用户重复进入服务器只更新该用户的端口号,并且不再修改IP信息,也不会再添加新的用户信息组。
在用户发送消息时服务器端的提示内容:
在这里插入图片描述
手机端安装"手机CAPP"app即可运行客户端程序:
在这里插入图片描述

总结

遇到的问题

服务器编程

  1. 数组信息错乱:在客户端发送过长的信息时,会导系统崩溃,并且在进行用户名查找时,对用户数组进行赋值会导致个别用户IP地址丢失。这个现象在Deepin系统中表现的非常明显,然而再Ubuntu中并不会出现这个问题。
    原因:根据我的推测这应该是属于变量的内存溢出问题,程序定义变量的过程实质上是内存的分配过程,当变量长度不确定(使用指针开辟数组)这时系统会从该内存号开始分配空间给变量。如果一个变量和下一个变量内存空间是相邻的,一旦某一个变量数据溢出就会导致数据覆盖到下一个变量的内存空间。由此可以推断,Deepin采用的是连续内存分配,而ubuntu采用了分散内存分配。
    解决:在进行服务器编程的时候,我使用了几个用指针方式开辟的数组变量,因此他们的长度并不确定。这里改变他的定义方式,限制他们的大小。或者将他们定义的顺序改变一下,都可以解决这个问题。

客户端编程

  1. 多进程模式导致系统崩溃:在使用多进程模式的时候,我使用了while循环创建子进程监听用户输入和消息接受,代码如下:
 while (flag)
    {
        ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
        pid = fork();
		if(pid == 0){
            if(recvbuf[0]=='>'){
                printf("欢迎:%s 进入房间\n",recvbuf);
            }  
        }
        else{
           
           if(recvbuf[0]!='>'){
             printf("%s\n",recvbuf);
                }  
            printf("发送:%s\n",sendbuf);
            fgets(sendbuf, sizeof(sendbuf), stdin);
            sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
        }
        memset(recvbuf, 0, sizeof(recvbuf));
        memset(sendbuf, 0, sizeof(sendbuf));

    }
 

这样一来程序可以正常执行,并且开始运行全部正常,但是再运行了几十条消息之后系统响应速度明显下降,最终只能强制关机。
原因:在函数fork()之后立即开始两个进程。分别执行fork=0子进程和fork=pid(子进程号)父进程。子进程会把整个程序资源和代码都复制一份,在另一个存储空间执行,它的资源将不再和父进程共享。
在我的这段代码中,在while语句里面创建了子程序,并且在子程序代码中没有while结束标志。这样一来子程序会一直在while创建子程序的子程序。每发送一条信息,或者每接收一条信息,都会创建一个子程序,以此维持对信息的不间断接收和对输入的阻塞等待。
但是!这样的程序是具有极大地弊端的。因为我们的子进程不会结束,并且不断创建新的子进程,当子进程创建下一个子进程后,上一个子进程其实已经没有作用了,但是他还在作为父进程维持当前的子进程,所以我们不能在父进程里面回收他的资源,但是如果加入子进程结束条件,则会导致无法持续监测接收数据。这就形成了一个两难之选:在这里我选择了牺牲系统资源。这样一来系统会出现非常多的僵尸进程。我们知道在系统中能够分配的进程号是有限的,一旦僵尸进程占用大量的进程号就会导致系统错乱,甚至崩溃。
比如著名的while-fork炸弹:

int main()  
{  
  while(1)  
    fork();  
 return 0;  
}  

你会发现这与我的代码结构十分相似,只不过这个语句是瞬间创造大量子进程崩溃你的系统,而我的代码是慢性崩溃你的系统。
解决:采用多线程方式实现客户端的代码设计;


  1. 关于IP地址的更多信息 ↩︎

  2. 这部分来自这位博主的博客 ↩︎

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。

更多推荐