二、TCP网络编程


随着网络的发展,网络通信必不可少,整体网络的实现是采取分层的方法实现的。应用层是对于要发送的数据的一种控制;传输层是两个进程间的通信,实现如何传数据。传输层协议:

  • TCP:面向连接的,可靠的,字节流服务
  • UDP:无连接的,不可靠的,数据报服务

今天我们学习传输层的TCP协议编程流程。

一、基本概念

TCP编程函数中的某些函数中的参数涉及到一些其他概念,为了理解函数不吃力,我们先看一些基础概念。

小知识点:

  1. errno 是记录系统的最后一次错误代码,是一个int型的值。
  2. 命令: uname -a。作用: 查看系统内核版本号及系统名称

(一)通用socket地址

socket含义是一个IP地址和端口,唯一标识了使用TCP通信的一端,一般称为套接字或socket地址。

socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:

# include <bits/socket.h>
struct sockaddr
{
     sa_family_t sa_family;//地址族类型变量
     char sa_data[14];//存放socket地址值
};

sa_family成员是地址族类型变量,地址族类型通常与协议族类型对应。常用的协议族(protocol family也称为domain)和对应的地址族的关系如下表

协议族 地址族 描述
PF_UNIX AF_UNIX UNIX本地域协议族,长度可达到108字节
PF_INET AF_INET TCP/IPv4协议族,6字节
PF_INET6 AF_INET TCP/IPv6协议族,26字节

故地址族的填写由当前使用的协议族决定。

可以看到sa_data[14]无法完全存储各种协议族的地址。因此Linux定义了下面这个新的socket地址结构体:struct sockaddr_storage

# include <bits/socket.h>
struct sockaddr_storage
{
     sa_family_t sa_family;//地址族类型变量
     unsigned long int_ss_align;//内存对齐
     char _ss_padding[128-sizeof(_ss_align)];
};

这个结构体提供了足够大的空间存放地址值。这两个结构体是通用的socket地址结构体。

(二)专用socket地址

通用结构体很不好用,比如设置与获取IP地址和端口号就需要执行繁琐的操作,所以Linux为各个协议提供了专门的socket地址结构体

【1. UNIX本地域协议族PF_UNIX:】 使用sockaddr_un地址结构体

# include <sys/un.h>
struct sockaddr_un
{
     sa_family_t sa_family;//地址族类型变量
     char sun_path[108];//文件路径名
};

【2. TCP/IP协议族PF_INET:】sockaddr_insockaddr_in6两个专用socket地址结构体,分别用于IPv4IPv6,我们现在使用的地址一般为IPv4,所以我们对sockaddr_in结构体说明,IPv6的感兴趣的可以自己去看。

struct sockaddr_in
{
   sa_family_t sin_family;//地址族:AF_INET
   u_int16_t sin_port;//端口号,要用网络字节序表示(下面详细讲解)
   struct in_addr sin_addr;//IPv4地址结构体,见下面
};
struct in_addr
{
    u_int32_t s_addr;//IPv4地址,要用网络字节序表示
};

所有专用socket地址类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有协议族底层的socket编程接口使用的都是 struct_sockaddr 结构体参数。即:
在这里插入图片描述
所以要记得强转,不然会出错。

(三)主机字节序(小端)和网络字节序(大端)

【1. 大、小端概念:】

字节序问题:现代CPU的累加器一次都能装载至少4字节,这里考虑32位机,即一个整数。那么这4字节在内存中排列的顺序将影响它被累加器装载成的整数的值。

字节序又分为大端字节序小端字节序

  • 大端字节序:指一个整数的高位字节(23~31)存储在内存的低地址处低位字节(0~7bit)存储在内存的高地址处大端字节序也称为网络字节序

  • 小端字节序:指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。 现代PC大多采用小端字节序,因此小端字节序称为主机字节序

在这里插入图片描述
使用大端:手机,虚拟机;使用小端:inter芯片

【2. 大、小端转换的原因:】

当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。解决办法是:

  • 发送端总是把要发送的数据转化为大端字节序数据后再发送。
  • 接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。

大端字节序也称为网络字节序它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证

要注意的是,即使是同一台机器上的两个进程,比如一个用C语言编写,一个用JAVA编写通信,也需要考虑大端字节序的问题。

【3.大(网络字节序)、小(主机字节序)端的转换函数】

# include<arpa/inet.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

htonl就是“host to network long"即将long类型的主机字节序转换为网络字节序。其他的解释一样,就是类型不一样,字母顺序不一样。

  • htonl函数一般用来转换IP地址。
  • htons函数一般用来转换端口号(在TCP编程sockaddr_in结构体的成员port端口号就需要这个函数转换

所以任何格式化的数据通过网络传输时,都应该使用这些函数来转换字节序。

(四)IP地址转换函数

通常,人们使用可读性好的字符串来表示IP地址,比如用点分十进制字符串表示IPv4地址,以及用十六进制字符串表示IPv6地址。但是编程时我们需要把它们转换为二进制才可以使用,但是在记录日志时则相反,我们需要把整数表示的IP地址转化为可读的字符串

那么就必须要将用点分十进制字符串表示的IPv4地址用网络字节序(大端)整数表示的IPv4地址进行转换,系统提供了三个函数

# include<arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp,struct in_addr* inp);
char* inet_ntoa(struct in_addr in);
  • inet_addr函数用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址,失败时返回INADDR_NONE。一般在TCP编程sockaddr_in结构体成员sin_addr即IPv4地址需要用到这个函数)
  • inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储于参数inp指向的地址结构中,成功返回1,失败返回0。
  • inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。

(五)端口号

我们说过,在网络中通讯的主角是运行在不同主机上的两个进程。可以通过IP地址标识主机,端口号标识进程。
我们看一下端口号的分类:

范围 含义
0~1023 被公认的服务占用了,用户不能使用,如Web服务占用80端口
1024~49151 用户自己使用的端口号,即自己定义TCP编程sockaddr_in结构体的成员port端口号
49152~65535 用于自动分配,如我们电脑安装的客户端程序

那我们常见的知名端口号有:

公认服务 端口号
FTP文件传输服务 21
SSH 22
Telnet终端仿真服务 23
SMTP简单邮件传输服务 25
DNS 域名解析服务
HTTP超文本传输服务 80
HTTPS加密的超文本传输服务 443
POP3邮局协议版本3 110
腾讯QQ 8000

在Linux下使用cat /etc/serveices可查看知名的端口号。

二、TCP概述

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

【1. TCP的特点】:

  • 面向连接的传输协议:每一次完整的数据传输都要经过建立连接、使用连接、终止连接的过程;
  • 可靠、出错重传、且每收到一个数据都要给出相应的确认,保证数据传输的可靠性;
  • TCP连接是基于字节流的,而非报文;传输单位为数据段,每次发送的TCP数据段大小和数据段数都是可变的;
  • 仅支持单播传输,支持全双工传输。

【2. TCP的优缺点】:

优点:

可靠,稳定。主要体现在:

  • TCP在数据传递之前,会有三次握手来建立连接连接;
  • 在数据传递时,采用校验和,序列号,确认应答,超时重发,流量控制,滑动窗口等机制保证了可靠,提高了性能。
  • 在数据传送完后,会断开连接以节约资源。

缺点:

  • 传输速度慢;因为在TCP传送数据前,要建立连接,耗费时间,数据传递中又适用了很多机制来保证其可靠,也会消耗大量的时间。
  • 效率低,占用系统资源多;它要维护所有所有传输连接,每个连接都会占用系统的CPU,内存等资源。
  • 易被攻击;因为其本身的机制,在三次握手确认连接时,容易受到DOS、STN洪泛攻击等。

【3. TCP适用场景】:

TCP 适用于对可靠性、数据的传输质量要求高,但对实时性要求不高的场景,如 HTTP、HTTPS、FTP 等传输文件的协议以及 POP、SMTP 等邮件传输的协议。

【4. 运行于 TCP 协议之上的协议】:

  • HTTP 协议:超文本传输协议,用于普通浏览
  • HTTPS 协议:安全超文本传输协议,身披 SSL 外衣的 HTTP 协议
  • FTP 协议:文件传输协议,用于文件传输
  • POP3 协议:邮局协议,收邮件使用
  • SMTP 协议:简单邮件传输协议,用来发送电子邮件
  • Telent 协议:远程登陆协议,通过一个终端登陆到网络
  • SSH 协议:安全外壳协议,用于加密安全登陆,替代安全性差的 Telent 协议

三、TCP网络编程函数

下面的函数都是Linux系统调用函数,我们需要学习如何使用系统调用。

Linux上一切皆文件,所以socket套接字也是文件,分类在设备文件中,文件标识符为s。所以socket就是一个可读,可写,可控制,可关闭的文件描述符。

(一)socket()创建

首先创建socket套接字,我们使用socket系统调用创建一个socket,函数原型为:

# include<sys/types.h>
# include<sys/socket.h>
int socket(int domin,int type,int protocol);
             //调用成功返回一个socket文件描述符,失败返回-1,并设置errno

参数:

  • domain参数告诉系统使用哪个底层协议族,我们在上面列出了协议族和对应的地址族。如果为 TCP/IP协议,参数设置为PF_INET(用于IPv4)或PF_INET6(用于IPv6);对于UNIX协议族,设置为PF_UNIX。
  • type参数指定服务器类型。服务器类型主要有:
    (1)SOCK_STREAM流服务,用于TCP。
    (2)SOCK_DGRAM数据报服务,用于UDP。
  • protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议一般都设置为0,标识默认协议

创建socket时,指定了它的地址族,但是并未指定使用该地址族中的哪个具体socket地址,所以需要下一步命名绑定。

(二)bind()命名绑定

将一个socket与socket地址绑定称为给socket命名。在服务器程序中,我们通常要命名绑定socket,因为只有命名后客户端才能知道该如何连接它,而客户端通常不需要命名socket,会采取匿名方式,即使用操作系统自动分配的socket地址。

命名socket的系统调用是bind,函数原型为:

# include<sys/types.h>
# include<sys/socket.h>
int bind(int sockfd,const struct sockaddr* my_addr,socklen_t addrlen);
           //成功返回0,失败返回-1,并设置errno

参数:

  • sockfd:为socket文件描述符,socket系统调用函数的返回值。
  • my_addr:表示将所指的socket地址分配给未命名的sockfd文件描述符,我们上面说过通用地址使用不方便,所以在这我们使用TCP/IP专用socket地址即sockaddr_in。使用前先定义结构体,再将参数传入,参数包含地址族,端口号,IPv4地址,需要用到上面说的函数进行一定的转换,具体如下:
    struct sockaddr_in ser_addr; //定义结构体
    memset(&ser_addr,0,sizeof(ser_addr));//清空结构体,全为0
    ser_addr.sin_family=AF_INET;//设置地址族,因为当前所用为TCP/IPv4,对应的地址族为AF_INET
    ser_addr.sin_port=htons(6000);//端口号,需要将short类型的主机字节序转化为网络字节序
    ser_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//将点分十进制IPv4转换为网络字节序整数标的IPv4地址,此地址为回环测试地址,也可以输入本机IP
    
    在bind中使用时,需要强制转换为sockaddr类型的,即:
    (struct  sockaddr*)&ser_addr
    
    因为socket编程底层都是sockaddr结构体。
  • addrlen参数指出该socket地址的长度。用sizeof即可得知。

那么bind的使用就是:

int res=bind(sockfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));

常见bind失败返回-1并设置errno的值和原因为:

  • EACCES:被绑定的地址是受保护的地址,仅超级用户才可以,如将socket绑定到知名服务端口(0~1023)上时,就会返回这个。
  • EADDRINUSE:被绑定的地址正在使用中。

(三)listen()启动监听

socket命名后,还不能马上接受客户连接,我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接:

# include<sys/socket.h>
int listen(int sockfd,int backlog);
              //成功返回0,失败返回-1,并设置errno

注意:listen不会阻塞,因为它只是启动监听

参数

  • sockfd参数: 指定被监听的socket,socket系统调用函数的返回值。
  • backlog参数: 表示内核监听队列的最大长度,监听队列的长度如果超过backLog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。backlog参数的典型值是5

内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态(未完成三次握手的SYN_RCVD),和完全连接状态(完成三次握手的ESTABLISHED)的socket的上限,即如下图:
在这里插入图片描述
但自内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限由/pro/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。

我们可以使用 命令netstat

netstat //查看服务器上连接的状态

监听队列中完整连接的上限通常比backlog值略大,可能多1个,也可能多两个,Linux上完整连接最多为(backlog+1)个,即如果backlog的值为5,那么在监听队列中,处于ESTABLISHED完全连接状态的连接有6个,如果还有其他连接,当6个满了后,它们都会处于SYN_RCVD半连接状态。

服务端通过listen调用来被动接受连接,我们把执行过listen调用处于LISTEN状态的套接字称为监听socket;所有处于ESTABLISHED状态的socket则称为连接socket。

(四)connect()建立连接

服务器通过listen调用被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接

# include<sys/types.h>
# include<sys/socket.h>
int connect(int sockfd,const struct sockaddr* serv_addr,socklen_t addrlen);
             //成功返回0,一旦成功建立连接,sockfd就唯一标识了这个连接,客户端就可以通过读写sockfd来与服务器通信
             //失败返回-1,设置errno

参数:

  • sockfd: 由socket系统调用返回的sockfd。
  • serv_addr: 是服务器监听的socket地址。注意定义socketaddr_in结构体存储的是服务器的socket信息,所以应该和服务器的初始化一样,而不是客户端的。
  • addrlen: 指定这个地址的长度。

所以一般使用为:

struct sockaddr_in ser;//定义socket地址,存储服务器socket地址
memset(&ser,0,sizeof(ser));
ser.sin_family=AF_INET;//地址族
ser.sin_port=htons(6000);//端口号
ser.sin_addr.s_addr=inet_addr("127.0.0.1");//IP地址
int res=connect(sockfd,(strcut sockaddr*)&ser,sizeof(ser));

connect的执行在listen之后,accept之前

(五)accept()接受连接

从listen监听队列中接受一个连接。函数原型为:

# include<sys/types.h>
# include<sys/socket.h>
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
       //成功时返回一个新的连接socket,该socket唯一的标识了被接受的这个连接,服务器可以通过读写该socket来与被接受连接对应客户端通信;
       //失败返回-1,设置errno

参数:

  • sockfd:执行过listen系统调用的监听socket。即socket系统调用返回的sockfd经过listen系统调用
  • addr参数:用来获取被接受连接的远端socket地址(即客户端),所以我们需要再定义一个socketaddr_in结构体来存储客户端的socket地址,在使用参数时,记得强转。
  • addrlen参数:为socket地址的长度,用sizeof即可。

一般使用为:

struct sockaddr_in cli_addr;//定义保存客户端socket地址的结构体
socklen_t len=sizeof(cli_addr);//得到长度
int clifd=accept(listenfd,(struct sockaddr*)&cli_addr,&len);//传入,执行成功后,cli_addr保存客户端的socket地址信息

accept只是从监听队列中取出连接,而不论连接处于何种状态(如ESYABLOSHED状态和CLOSE_WAIT状态),更不关心任何网络状况的变化,就算客户端断网了,它还是会正常返回。

(六)recv()读取、send()发送数据

对文件的读写操作read和write同样适用于socket,到那时socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制,其中用于TCP流数据读写的系统调用是:

# include<sys/types.h>
# include<sys/socket.h>
ssize_t recv(int sockfd,void* buf,size_t len,int flags);//读取数据
ssize_t send(int sockfd,const void*buf,size_t len,int flags);//发送数据
            //成功返回实际读取/写入的数据长度,出错返回-1,设置errno

参数

  • sockfd为accept函数返回的sockfd,表示被接受的客户端,可以通过socfkd对他进行读写,注意不是socket函数返回的sockfd。
  • buf:缓冲区位置
  • len:缓冲区大小
  • flags:为数据收发提供了额外的控制,一般设置为0。

recv函数成功返回 实际读取到的数据的长度,它可能小于我们期望的长度len,因此我们可能要多次调用recv,才能读取到完成的数据。recv可能返回0,这意味着通信对方已经关闭连接,没有读到数据。send函数往sockfd表示的客户端写入数据,如下图所示:
在这里插入图片描述

(七)close()关闭连接

关闭连接实际上就是关闭连接对应的socket,和关闭普通文件描述符方法一样,系统调用如下:

# include<unistd.h>
int close(int fd);
    //成功返回0,失败返回-1,设置errno

fd参数: 是待关闭的socket。

但是close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1只有当fd的引用技术为0时,才能真正的关闭。这个概念我们在父子进程共享文件时也说过,即一次fork系统调用默认将使父进程中打开的socket的引用计数加一,所以必须在父、子进程中都对该socket执行close调用才能将连接关闭。

close这种关闭方法是专门为了网络编程设计的,如果要立即终止连接,而不是将socket的引用计数减一,可以使用shutdown调用,它可以关闭读、写或全部关闭。

四、TCP网络编程流程

(一)编程流程

现在我们需要将进程分为:服务器(主动),客户端(被动)两种类型,其中:

  • 服务器和多个客户器连接,所以服务器复杂。
  • 客户器和一个服务器连接。所以客户器简单。

我们可以根据TCP编程函数写出服务器和客户端编程所用的函数流程:

服务器:

int socket();创建一个用于监听客户端连接的网络套接字《----》买手机
int bind();将创建的套接字与本端的地址信息进行绑定IP+端口《----》给手机插卡,绑定电话号码,不然别人无法拨号联系我
int listen();启动监听,不会阻塞《----》开机,不用一直等着别人给你打电话
int accept():接受一个客户端的连接,返回的是一个客户端连接套接字《----》如果有人打电话,我就接听电话
int recv()/send();读取数据或者发送数据《----》交谈
int close();关闭文件描述符《----》挂电话

客户端:

int socket();创建一个用于整个通讯的套接字《----》买手机
int connect();与服务器程序建立连接《----》拨号
int recv()/send();读取或发送数据《----》交谈
int close();关闭连接《----》挂电话

但是这样的流程会出现很多问题,如下:

  • 电话接听后,两个人同时说话,就会无法交谈。即必须规定客户端和服务器发送数据的顺序。
  • 不能说一句话挂一次电话,再拨号再说下一句。所以循环多次进行数据交互,故recv/send必须在while循环内,说完后我再挂断电话
  • 不能接听一个人的电话就关机一次,也不能只接听一个人的电话。所以服务器要一直开启,循环接受处理客户端连接,故accept在while循环内,直到全部结束,关闭监听套接字。

所以对整个流程进行一个改进,用伪代码进行一个描述:
在这里插入图片描述

(二)编程模型图

我们将整个编程流程画图展示:
在这里插入图片描述

(三)编程实例

实现一个简单的TCP通信,客户端和服务器建立连接,发送数据,服务器收到数据,回复信息。
【Tcpcli.c】

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


int main()
{
    int listenfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
    assert(listenfd!=-1);

    struct sockaddr_in ser_addr;//定义服务器TCP/IP专用socket地址
    memset(&ser_addr,0,sizeof(ser_addr));//置为空,防止下一个客户端无法连接
    ser_addr.sin_family=AF_INET;//设置地址族
    ser_addr.sin_port=htons(6000);//设置端口号,将主机字节序转换为网络字节序
    ser_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//设置IPv4,将点分十进制转换为网络字节序整数表示的IPV4地址
    int res=bind(listenfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));//命名绑定socket
    assert(res!=-1);

    res=listen(listenfd,5);//监听,监听队列为5
    assert(res!=-1);

    //循环接受客户端连接,
    while(1)
    {
        struct sockaddr_in cli_addr;//定义客户端socket地址
        socklen_t len=sizeof(cli_addr);

        int clientfd=accept(listenfd,(struct sockaddr*)&cli_addr,&len);//接收一个客户端
        if(clientfd==-1)
        {
            printf("one client link error\n");
            continue;
        }
        printf("one client success--%s:%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
        //循环接受,发送数据
        while(1)
        {
            char buff[128]={0};
            int num=recv(clientfd,buff,127,0);//接收数据
            if(num==-1)//接收失败
            {
                printf("recv error\n");
                break;
            }
            else if(num==0)//客户端关闭
            {
                printf("client over\n");
                break;
            }
            printf("recv data is:%s\n",buff);

            char* restr="recv data success";
            num=send(clientfd,restr,strlen(restr),0);//回复数据
            if(num==-1)
            {
                printf("send data error\n");
                break;
            }
        }
        close(clientfd);//关闭客户端连接,服务器还可以接收下一个客户端
    }
    close(listenfd);//关闭服务器
    exit(0);
}


【Tcpcli.c】

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

int main()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
    assert(sockfd!=-1);

    //客户端必须定义服务器的,否则无法连接
    struct sockaddr_in ser_addr;//定义服务器TCP/IP专用socket地址
    memset(&ser_addr,0,sizeof(ser_addr));//置为空,防止下一个客户端无法连接
    ser_addr.sin_family=AF_INET;//设置地址族
    ser_addr.sin_port=htons(6000);//设置端口号,将主机字节序转换为网络字节序
    ser_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//设置IPv4,将点分十进制转换为网络字节序整数表示的IPV4地址
    
    int res=connect(sockfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));//连接服务器
    assert(res!=-1);
    //循环接受客户端连接,
    while(1)
    {
        printf("please input:");
        char data[128]={0};
        fgets(data,127,stdin);
        if(strncmp(data,"bye",3)==0)
        {
            break;
        }
        int num=send(sockfd,data,strlen(data)-1,0);
        assert(num!=-1);
        if(num==0)
        {
            printf("send length is zero\n");
            break;
        }
        char buff[128]={0};
        int n=recv(sockfd,buff,127,0);
        assert(n!=-1);
        if(n==0)
        {
            printf("error\n");
            break;
        }
        printf("recv ser data is:%s\n",buff);
    }
    close(sockfd);//关闭服务器
    exit(0);
}

运行一个客户端和服务器连接,正常通信。

在这里插入图片描述
再运行一个会发现,没有和服务端连接,发送的数据也不能发过去,这是因为目前我们的服务器在一个时间只能连接一个客户端,所以第二个客户端不能被连接。

在这里插入图片描述

当我们关掉第一个客户端,可以看到第二个客户端成功连接,发出的信息也成功被收到。
在这里插入图片描述

加油哦!💪。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章