16【TCP、UDP、网络通信】

一、基于网络编程

1.1 网络编程概述

计算机网络是通过传输介质(网线)、通信设施(路由器、交换机等)和网络通信协议,把分散在不同地点的计算机设备互连起来的,用来实现数据共享。

网络编程就是编写程序使互联网的多个设备(如计算机)之间进行数据传输。Java语言对网络编程提供了良好的支持。通过其提供的接口我们可以很方便地进行网络编程。

1.2 网络分层

通过网络发送数据是一项复杂的操作,通过网络将数据从一台主机发送到另外的主机,这个过程是通过计算机网络通信来完成。

网络通信的不同方面被分解为多个层,通信的双方具有相同的层次,层次实现的功能由协议数据单元来描述。不同系统中的同一层构成对等层,对等层之间通过对等层协议进行通信,理解批次定义好的规则和约定。将网络分层,这样就可以修改甚至替换某一层的软件,只要层与层之间的接口保持不变,就不会影响到其他层。

1.2.1 OSI参考模型

世界上第一个网络体系结构在1974年由IBM公司提出,名为SNA。以后其他公司也相继提出自己的网络体系结构。为了促进计算机网络的发展,国际标准化组织ISO在现有网络的基础上,提出了不基于具体机型、操作系统或公司的网络体系结构,称为开放系统互连参考模型,即OSI/RM(Open System Interconnection Reference Model)。

1) 物理层

为数据端设备提供原始比特流的传输的通路;网络通信的数据传输介质,由电缆与设备共同构成。常见:中继器,集线器(HUB)、网线、RJ-45标准等

物理层主要定义物理设备标准,如网线的接口类型、光纤的接口类型、集线器的工作原理、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后在转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特

物理层定义了网线、集线器等设备的标准,有了网线我们可以进行数据的点对点传输:

当需要网络通信的用户比较多时,需要给所有的用户都拉一根网线进行通信,这显然不现实;

因此集线器就出来了,集线器用于收集多个通信用户,简化多个通信设备之间的通信流程;

集线器属于纯硬件网络底层设备,基本上不具有交换机"智能记忆"能力和"学习"能力,也不具备交换机所具有的MAC地址表。它发送数据时没有针对性的,采用广播方式发送。当它要向某节点发送数据时,不是直接把数据发送到目的节点,而是把数据包发送到与集线器相连的所有节点。

【例如A节点发送数据到D节点】

A将数据广播到集线器1的所有端口,然后集线器1通过port2将数据广播到集线器2的所有端口,这样一来连接在集线器中的所有通信设备都将会收到来自A节点的比特(数据);然后由节点自身判断是否是自身所需要的数据,如果不是则丢弃;

因此在只有物理层的设备/标准/协议,进行通信时,网络通信不够灵活,要使得网络通信更加灵活高效,就需要新增一些新的设备/标准/协议;

2) 数据链路层

我们首先看只有物理层通信时所面临的问题:

  • 1)无法定向发送数据:只能广播式,然后由节点自身判断该数据是否是发送给我的,如果不是则丢弃
  • 2)数据流量控制:当发送端发送的速度和接收端接收能力的不匹配时,会造成传输错误,因此在接收端应该具备流量控制的功能;例如接收方收不下就不回复确认,让发送端不要再发等等..

数据链路层:在通信的实体间建立数据链路连接;将数据分帧、流量控制、差错校验、物理地址寻址、重发等。常见:网卡,网桥,二层交换机等;

在数据链路层中,为每一个互联网设备同规划了一个Mac地址,用于标识一台唯一机器;并提供物理寻址(实现点对点的数据传输)功能;

【模拟A发送数据给B】

将数据发送给port2端口,然后由port2端口将数据发送给port6端口,完成数据的发送;

数据链路层提供的功能:

  • 1)流量控制:发送端发送数据时等待接收端响应,当发送端的发送速率大于接收端的接收速率时,接收方收不下就不回复确认,让发送端不要再发
  • 2)差错校验:网络设备在高速通信时,可能由于各种原因,导致数据的传输出现错误,数据链路层通过一定的协议/标准/算法进行差错校验
  • 3)物理地址寻址:在数据链路层规范了Mac地址,使得数据可以点对点进行发送

Tips:Mac地址也叫物理地址,是网卡在出厂时,厂商设定的唯一标识,用于唯一标识一台通信设备;相当于通信设备的地址,用于网络通信时使用;;

3) 网络层

上述案例中,都是节点数量相对较少的局域网中进行网络通信,而当我们通信规模非常庞大时,例如互联网,这时就无法仅仅是通过Mac地址的寻址来找到对方机器进行通信了(通信效率低);

网络层:为数据在结点之间传输创建逻辑链路,并分组转发数据;对子网间的数据包进行路由选择。常见:路由器、多层交换机、防火墙、IP、IPX、RIP、OSPF

网络层主要解决在庞大的互联网结构体系中,如何找到对方的主机进行通信,并提出IP地址来取代Mac地址的定位,也就是说在互联网中寻找对应的主机是通过IP地址来寻找;

4) 传输层

传输层:提供应用进程之间的逻辑通信;建立连接,处理数据包错误、数据包次序。常见:TCP、UDP、SPX、进程、端口(socket)

网络层主主要的功能是在互联网这张大网中寻找到对应的主机,寻找到后,建立通信管道,就要开始通信了,传输层主要的功能是保证数据的传输,例如如何传输?传输的方式是什么?如何保证数据可靠传输?等问题;

5) 会话层

会话层:建立端连接并提供访问验证和会话管理;为数据在结点之间传输创建逻辑链路,并分组转发数据;使用校验点可使会话在通信失效时从校验点恢复通信。常见:服务器验证用户登录、断点续传

其主要功能如下:

  • 维护应用程序之间的会话连接:该层负责管理和维护客户端与服务器之间的连接。例如,在Web服务器上登录时,会话层将会话信息保存在服务器上,以便用户访问页面时能够自动登录。
  • 安全性:为了保证安全性,会话层还支持身份验证和加密等功能。

6) 表示层

表示层:提供数据格式转换服务;解密与加密,图片解码和编码、数据的压缩和解压缩。常见:URL加密、口令加密、图片编解码

7) 应用层

应用层:访问网络服务的接口;常见:Telnet、FTP、 HTTP、 SNMP、DNS等

通过传输层之后,数据已经能从一台计算机的某个应用程序发送到另一台计算机的某个应用程序了。接下来考虑的是完成数据的通信来达到应用程序本身的功能;

会话层、表示层、应用层这三层的功能都是面向应用程序的,比如会话层的用户登录、断点续传,表示层的加密与解密、编码解码,会话层的交互界面、访问网络接口等。这些功能直接对应用程序本身提供服务的。换句话来说,我们开发不同的应用程序可以使用不同的应用层协议来达到一些功能,也可以不使用任何的应用层协议,自己单独定义一些通信规则来开发自己的应用程序。

1.2.2 数据网络传递过程

  • 1)当客户端要发送数据到服务器时,数据首先被应用层封装成了报文(也叫上层数据);
  • 2)数据来到传输层,在报文的基础上在加上源端口目的端口被封装成了段(Segment)

  • 3)之后来到网络层,在段的基础上加上源IP目的IP被封装成包(Package)

  • 4)通过目标IP找到对应主机后,进入数据链路层,通过APR广播找到IP主机对应的Mac地址,在包的基础上加上源Mac目标Mac组成帧(Frame)

  • 5)到了物理层后,读取帧中的Mac地址进行物理寻址,最终将比特流发送到目的主机;

各通信层次数据名称小结:

1.2.3 TCP/IP参考模型

需要注意的是OSI只是一个参考模型,给我们在搭建网络时提供一个理论支撑以及分析,在实际搭建搭建网络时并不一定要要划分为7个层次,当今互联网普遍使用的是TCP/IP参考模型;

在TCP/IP,即Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,是Internet最基本的协议,Internet国际互联网络的基础。

TCP/IP协议是一个开放的网络协议簇,它的名字主要取自最重要的网络层IP协议和传输层TCP协议。TCP/IP协议定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。TCP/IP参考模型采用4层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求,这4个层次分别是:网络接口层、网络层(IP层)、传输层(TCP层)、应用层

1.3 网络名词

1.3.1 IP地址

IP(Internet Protocol Address):全称互联网协议地址,简称IP地址;IP地址用于标识互联网上的唯一一台机器,互联网就是通过IP地址锁定到我们的这台电脑,相当于家庭地址;

IP地址的分类

  • IPv4:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d 的形式,例如192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。
  • IPv6:由于互联网的网民日益增多,IPv4的IP地址资源有限。为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。

  • 查看本机IP地址,在cmd控制台输入:
ipconfig
  • 检查网络是否连通,在cmd控制台输入:
ping 空格 IP地址
ping 163.177.151.109

Tips:在每台计算机出厂时,都有一个用于标识自己电脑的地址:127.0.0.1,和一个本机域名:localhost

本机hosts:C:\Windows\System32\drivers\etc

1.3.2 端口号

在两台计算机通信时,更准确的来说是两台计算机的某个进程(应用程序)在通信,IP地址可以唯一标识网络中的设备,那么端口号就是唯一标识计算机中的某个应用程序了;

Tips:端口号的取值范围为0~65535。其中0~1023之间的端口号用于计算机内置的一些进程,我们自己的程序的端口号尽量设置在1023以上的端口,保证端口不会占用冲突;

二、传输协议

如同人与人之间相互交流是需要遵循一定的规则(如语言)一样,计算机之间能够进行相互通信是因为它们都共同遵守一定的规则,即网络协议。

OSI参考模型和TCP/IP模型在不同的层次中有许多不同的网络协议,如图所示:

我们今天主要讨论的是传输层的协议,即考虑应用程序之间的逻辑通信。简单来说就是数据该如何发送给其他机器;

2.1 UDP传输协议

UDP(User Datagram Protocol):用户数据报协议;UDP是面向无连接的通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。

2.1.1 UDP传输过程

UDP是面向报文传递数据的;在UDP传输过程中,分别为发送端接收端

发送端使用UDP发送数据时,首先将其包裹成一个UDP报文(包含数据与首部格式)通过网络将其发送给接收端;接受端接收到UDP报文后,首先去掉其首部,将数据部分交给应用程序进行解析;

需要注意的是,UDP不保证数据传递的可靠性,在传递过程中可能出现丢包等情况,另外,即使接收方不存在报文依旧被发送出去(丢包)。但正是因为UDP不需要花费额外的资源来建立可靠的连接,因此UDP传输速度快,资源消耗小

2.1.2  UDP报文格式

一个完整的UDP报文包含首部载荷(数据)两部分,首部由 4 个 16 位长(2 字节)的字段,共8个字节组成,分别说明该报文的源端口、目的端口、报文长度和校验值。

UDP 报文中每个字段的含义如下:

  • 源端口:发送端所使用应用程序的UDP端口,在接受端的应用程序里,由这个字段的值作为响应的目的地址;这个字段是可选的,所以发送端的应用程序不一定会把自己的端口号写入该字段中。如果不写入端口号,则把这个字段设置为 0。这样,接收端的应用程序就不能发送响应了。
  • 目的端口:接收端计算机上 UDP 软件使用的端口。
  • 长度:表示 UDP 数据报长度,单位:字节;包含 UDP 报文头+UDP 数据长度。因为 UDP 报文头长度是 8 个字节,所以这个值最小为 8。
  • 校验值:一些二进制码,可以检验数据在传输过程中是否被损坏。

2.1.3  UDP总结

由于使用UDP协议消耗资源小,通信效率高;因此一般用于实时性要求比较高的场合如:音频、视频的传输等;例如视频会议都使用UDP协议,如果出现了网络丢包情况也只是造成卡帧现象,对整体影响不大

但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。

2.2 TCP传输协议

TCP(Transmission Control Protocol):传输控制协议;TCP协议是面向连接的通信协议;即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。

在TCP连接中必须要明确客户端(发送端)与服务器端(接收端),由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”;

2.2.1 TCP报文格式

一个完整的TCP报文同样也是由首部数据载荷组成;TCP的全部功能都体现在它首部中各字段的作用。

  • 源端口:占2个字节,16个比特;表示发送该报文的应用程序端口号,用于接收端的响应;
  • 目的端口号:占2个字节,16个比特;标识接受该TCP报文的应用程序端口号;
  • 序号:数据载荷中的数据都是有顺序的,序号用于标识发送端向接收端发送的数据字节流的位置;

序号占4个字节,32个比特位,取值范围2^32-1,序号增加到最后一个时,下一个序列号又回到0;

  • 确认号:期望收到对方下一个TCP报文序号的起始位置,同时也是对之前收到的数据进行确认;

确认号和序号一样,占4个字节,32个比特位,取值范围2^32-1,确认号增加到最后一个时,下一个确认号又回到0;

A向B发送数据:

  • 1)下一次B发送给A的情况:
    • 确认号:B要保证全部接收到A发送的信息,那么下一次发送给A报文中的确认号应该是201+100=301,代表A发送过来的0-300的数据B都接收了;
    • 序列号:由于A发送给B的确认号为800,代表0-799的数据A都正常接收了,因此B下一次给A发送的序号为800
  • 2)上一次B发送给A的情况:
    • 确认号:由于A报文中的序号是201,因此B上一次给A发送的确认号为201,代表B接收到了A的0~200的数据,下次A直接给B发送201位置的数据即可;
    • 序列号:A的确认号为800,代表B上一次发送给A的序号+数据长度=800,因此B上一次发送给A的序列号 = 800 - 数据长度

B向A发送数据:

若确认号=n,则表明,序号n-1为止的所有数据都已正确接收,期望接收序号为n以及之后的数据;

本次的序列号 = 上次的确认号

本次的确认号 = 上次的序列号 + 数据载荷的长度

  • 数据偏移:占4个比特,用来指出数据载荷部分的起始处距离报文的起始处有多远;也就是TCP首部的长度。需要注意的是数据偏移以4个字节为1个单位;
  • 填充:由于选项的长度可变,因此用来填充的确认报文首部能被4整除(因为数据偏移字段,也就是首部长度字段,是以4字节为1个单位的);

如图:

  • 保留字段:占6个比特,保留为今后使用,但目前为0;
  • 窗口:占2个字节,16个比特;用于流量控制和拥塞控制,表示当前接收缓冲区的大小。在计算机网络中,通常是用接收方的接收能力的大小来控制发送方的数据发送量。TCP连接的一端根据缓冲区大小确定自己的接收窗口值,告诉对方,使对方可以确定发送数据的字节数。
  • 校验和:占2个字节,16比特;检查报文的首部和数据载荷两部分,底层依赖于具体的校验算法;
  • 紧急指针:占2个字节,16比特;用来指明紧急数据的长度;当发送端有紧急数据时,可将紧急数据插队到发送缓存的最前面,并立刻封装到一个TCP报文段中进行发送。紧急指针会指出本报文段数据载荷部分包含了多长的紧急数据,紧急数据之后是普通数据
  • 选项:附加一些额外的首部信息;
  • 标志位
    • ACK(确认):取值为1时确认号字段才有效;取值为0时确认号字段无效,一般情况下都为1;
    • SYN(同步):在连接建立时用来同步序号
    • FIN(终止):为1时表明发送端数据发送完毕要求释放连接
    • RST(复位):用于复位TCP连接,值为1时说明连接出现了异常,必须释放连接,然后再重新建立连接,有时RST置1还用来拒绝一个非法的报文段或拒绝打开一个TCP连接;
    • PSH(推送):为1时接收方应尽快将这个报文交给应用层,而不必等到接受缓存都填满后再向上交付
    • URG(紧急):为1时表明紧急指针字段有效,取值为0时紧急字段无效;

2.2.2 三次握手

1) 三次握手的原理

由于TCP是基于可靠通信的,在发送数据之前必须建立可靠的连接;TCP建立连接的过程分为三个步骤,我们称为"三次握手";

简单的过程如下图所示:

  • 1)第一次握手:发送端向接收端端发出连接请求,等待接受的响应。
  • 2)第二次握手,接收端向发送端响应,通知发送端收到了连接请求。
  • 3)第三次握手,客户端再次向服务器端发送确认信息,确认连接。整个交互过程如下图所示。

我们结合TCP报文原理来具体分析一下三次握手的详细流程:

原理图如下:

  • 第一次握手

首先客户端使用向服务端发送TCP连接请求(此报文不携带载荷,只有首部),本次请求中SYN标记为1,表明本次是一个连接请求。序号标记为x,作为本次TCP连接的客户端起始序号值;

Tips:在TCP协议中规定,SYN标记为1的报文不可以携带数据,但还是要消耗掉一个序号

  • 第二次握手

服务端接收到客户端的请求后,如果确定建立连接,则向客户端发送一个确认报文。该报文SYN标记为1,表明本次是一个连接请求。ACK标记为1,表明本次是一个确认请求。

综合本次请求的含义为:连接确认请求,即服务端收到客户端请求之后,来与客户端建立连接,表明同意与客户端建立本次TCP连接;

本次请求序号标记为y,作为本次TCP连接服务端的起始序号值。确认号为x+1,这是对上一次请求初始序号x的确认。

  • 第三次握手

客户端再次向服务端发送确认报文,该报文中ACK标记为1,表明本次是一个确认报文。

本次确认报文的序号为x+1,这是因为第一次TCP报文的序号为x,并且不携带数据,因此本次序号为x+1

本次确认报文的确认号为y+1,这是针对服务端请求初始序号y的确认。

2) 为什么要三次握手

在三次握手中,为什么客户端最后还需要发送一个确认报文呢?难道在服务端响应确认报文之后不能确定TCP连接已经建立成功吗?

即:为什么要三次握手而不是"两次握手"呢?

我们观察下图:

①客户端发送TCP连接请求到服务端,想要建立连接,但由于本次请求超时了

②客户端再次发送一个新的TCP连接请求到服务端

③服务端接收到客户端刚刚发送的TCP连接请求,开始做出回应

④客户端接收到服务端的回应,连接建立成功

⑤过了一段时间后,客户端像服务端发送断开连接请求(进入四次挥手过程,暂时不讨论)

⑥服务端与客户端断开连接后,突然收到之前客户端发送的超时请求,但服务端还以为是客户端刚发送的连接请求,因此对该请求进行确认

⑦由于是"两次握手",并不需要等到客户端对本次连接做出回应,本次连接就建立成功了。这无疑是浪费了服务端的连接资源

因此"三次握手"主要是为了防止失效的连接请求报文突然又传送到了服务端而造成错误;

2.2.3 四次挥手

1) 四次握手的原理

TCP建立连接时需要"三次握手",断开连接时则需要"四次挥手";

原理图如下:

【第一次挥手】

客户端向服务端发送连接释放报文

  • FIN标记为1:表明本次为连接释放报文
  • ACK标记为1:对之前收到的报文进行确认
  • 序号标记为u:u为之前服务端接收到客户端的最后一个数据+1
  • 确认号标记为v:v为客户端接收到服务端的最后一个数据+1

Tips:TCP规定,释放连接报文(FIN标记为1的报文)即使不携带数据也要消耗一个序号;

【第二次挥手】

服务端接收到来自客户端的连接释放报文,由于服务端有可能正在该向客户端发送其他数据(还有数据未发送完),因此服务端不能立即发送释放连接报文,而是先向客户端发送一个确认报文表明连接释放报文已经被成功接收;

  • ACK标记为1:对之前发送的连接释放报文进行确认
  • 序号为v:从上一次确认号的数据位置开始发送数据
  • 确认号为u+1:是对上一次连接释放报文中序号u的确认

【第三次挥手】

服务端确认自身没有数据要发送客户端或者已经将数据全部发送完毕之后,开始发送连接释放报文给客户端,代表确认连接断开;

  • FIN标记为1:表明本次是一个连接释放报文,服务端与客户端断开TCP连接
  • ACK标记为1:对之前收到的报文进行确认
  • 序号为w:是对之前序号u的补充,因为期间服务端有可能发送了很多数据到客户端
  • 确认号为u+1:说明客户端在此期间并没有发送数据到服务端

【第四次挥手】

客户端接收到来自服务端的连接释放报文开始回复服务端的响应,服务端接收到响应后服务端的TCP连接释放;但客户端进入2MSL时间等待窗口,时间窗口到达后,客户端关闭TCP连接;

  • ACK标记为1:表明是对服务端报文的确认
  • 序号为u+1:从上一次确认号的位置开始发送
  • 确认号为w+1:是对服务端发送的连接释放报文的确认

MSL(Maximum Segment Lifetime):报文最大生存时间,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。RFC793建议为2分钟,对于当前的网络环境,TCP允许不同的实现使用更小的MSL值;

2) 为什么要四次挥手

  • 为什么服务端收到客户端的连接释放报文报文后不直接发送连接释放报文来关闭TCP连接,从而变成三次握手?

服务端刚收到来自客户端的连接释放报文后,有可能还再向此客户端发送数据,必须等到服务端发送完毕后再发送连接释放报文给客户端。因此这一步的"握手"并不能省略。

  • 为什么客户端接收到了服务端的连接释放报文后不能直接关闭TCP连接,从而变成三次握手?

如果少了最后一步的客户端确认动作,那么服务端无法得知客户端是否接收到服务端的连接释放报文。并且在客户端发送完最后一次确认报文给服务对后客户端的TCP连接进入2MSL时间等待窗口,这是因为有可能客户端发送的确认报文超时了,此时服务端必定会要求客户端重传,因此客户端不能在发送确认报文完毕后就立即释放TCP连接,而是要进入一个时间等待窗口。

2.2.4 TCP总结

使用TCP协议传输数据时,必须要建立可靠连接(三次握手),当连接关闭时还需要四次挥手,对性能损耗较大,如果频繁的创建和关闭TCP连接性能势必会有很大影响。但是由于TCP的可靠传输,对一些数据完整性要求较高的场合比较适用,如文件上传下载等;

2.3 UDP和TCP小结

  • UDP:
    • 1)面向无连接,资源消耗小,传输速度高
    • 2)不保证数据的完整性,可靠性低,安全性低
    • 3)应用场景,在传输速度要求较高(实时性要求较高)的场景下使用,对数据的完整性没有要求;
  • TCP:
    • 1)面向连接,每次连接都需要三次握手,每次断开连接都需要四次挥手,资源消耗大,传输速度低
    • 2)保证数据的完整性,可靠性高,安全性高
    • 3)应用场景,对数据的完整性有要求(可靠投递)

三、Java实现UDP应用程序

3.1 InetAddress类

java.net.InteAddress类是用于描述IP地址和域名的一个Java类;

常用方法如下:

  • public static InetAddress getByName(String host):根据主机名获取InetAddress对象
  • public String getHostName():获取该对象对应的主机名
  • public String getHostAddress()获取该对象对应的IP地址

示例代码:

package com.dfbz.demo01_实现UDP;

import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_InetAddress {
    public static void main(String[] args) throws UnknownHostException {
        // 根据域名获取InetAddress对象
        InetAddress localhost = InetAddress.getByName("localhost");
        
        // 获取主机名
        String hostName = localhost.getHostName();
        
        // 获取ip地址
        String hostAddress = localhost.getHostAddress();

        System.out.println(hostName);               // localhost
        System.out.println(hostAddress);            // 127.0.0.1

        System.out.println("-----------");
        
        InetAddress baidu = InetAddress.getByName("www.baidu.com");
        System.out.println(baidu.getHostName());        // www.baidu.com
        System.out.println(baidu.getHostAddress());     // 14.119.104.254
    }
}

3.2 DatagramPacket类

java.net.DatagramPacket是一种UDP协议的数据包结构,它包含源地址、目标地址和要传输的数据,用于封装一个UDP数据报文。

使用DatagramPacket,可以在应用程序之间发送和接收UDP数据包。

3.2.1 构造方法

  • public DatagramPacket(byte[] buf, int length, InetAddress address, int port):创建一个数据包对象
    • buf:要发送的内容
    • length:要发送的内容⻓度,单位字节
    • address:接收端的ip地址
    • port:接收端⼝号
  • public DatagramPacket(byte buf[], int length):创建一个数据包对象

示例代码:

package com.dfbz.demo01_实现UDP;

import java.net.DatagramPacket;
import java.net.InetAddress;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_DatagramPacket_构造方法 {
    public static void main(String[] args) throws Exception {
        // 创建一个UDP报文用于发送
        DatagramPacket packet = new DatagramPacket(
                "abc".getBytes(),                       // 报文封装的数据
                "abc".getBytes().length,                // 报文的数据长度
                InetAddress.getByName("localhost"),     // 接收端的地址
                8989                                    // 端口
        );

        // 创建一个UDP报文用于接收
        byte[] data = new byte[1024];
        DatagramPacket packet2 = new DatagramPacket(
                data,
                data.length
        );
    }
}

3.2.2 常用方法

  • public synchronized int getLength():获取此UDP数据包载荷的数据长度(单位字节)
  • public synchronized int getPort():获取此UDP数据包的目的端口号
  • public synchronized byte[] getData():获取此UDP数据包的载荷部分(数据)

示例代码:

package com.dfbz.demo01_实现UDP;

import java.net.DatagramPacket;
import java.net.InetAddress;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo03_DatagramPacket_成员方法 {
    public static void main(String[] args) throws Exception {

        // 创建一个UDP报文用于发送UDP报文
        DatagramPacket packet = new DatagramPacket(
                "abc".getBytes(),
                "abc".getBytes().length,
                InetAddress.getByName("localhost"),
                8989
        );

        byte[] data = packet.getData();             // 获取UDP报文的数据
        int length = packet.getLength();            // 获取UDP报文的数据长度
        InetAddress address = packet.getAddress();  // 获取该UDP报文要发送的地址
        int port = packet.getPort();                // 获取该UDP报文要发送的端口

        System.out.println("getData(): " + new String(data));             // abc
        System.out.println("getLength(): " + length);                     // 3
        System.out.println("getAddress(): " + address.getHostAddress());  // 127.0.0.1
        System.out.println("getPort(): " + port);                         // 8989
    }
}

3.2 DatagramSocket类

java.net.DatagramSocket它提供了发送和接收UDP数据包的功能,用于描述一个UDP发送端或接收端;

在 Java 中,可以通过 DatagramSocket 类来创建一个套接字,并且可以通过 socket 的 receive 和 send 方法来发送和接收 UDP 数据包。这些方法提供了更好的灵活性,可以让您轻松地从多个地址和端口接收 UDP 数据包,并且可以选择性地发送数据包到特定地址和端口。 总的来说,DatagramSocket 是 UDP 协议的一种高级抽象,可以让您方便地在 Java 应用程序中发送和接收 UDP 数据包。

3.2.1 构造方法

  • public DatagramSocket(int port):通过端口构建一个发送端/接收端

示例代码:

DatagramSocket socket = new DatagramSocket(6969);

3.2.2 常用方法

  • public void send(DatagramPacket p):发送一个UDP数据包
  • public synchronized void receive(DatagramPacket p):接收一个UDP数据包
  • public void close():释放该Socket占用的资源

3.3 设计UDP应用程序

  • 发送端:
package com.dfbz.demo01_实现UDP;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo04_发送端 {
    public static void main(String[] args) throws Exception {

        // 创建一个UDP报文的发送/接收器
        DatagramSocket socket = new DatagramSocket();

        // 封装一个UDP报文
        byte[] data = new byte[8192];
        DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("localhost"), 9999);

        // 设置UDP报文的数据
        packet.setData("你好".getBytes());
        packet.setLength("你好".getBytes().length);

        // 发送UDP报文
        socket.send(packet);

        // 关闭资源
        socket.close();
    }
}
  • 接收端:
package com.dfbz.demo01_实现UDP;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo05_接收端 {
    public static void main(String[] args) throws IOException {

        // 创建一个UDP报文的发送/接收器
        DatagramSocket socket = new DatagramSocket(9999);

        // 创建一个UDP报文用于接收数据
        byte[] data = new byte[8192];
        DatagramPacket packet = new DatagramPacket(data, data.length);

        /*
         将socket接收到的UDP报文赋值给packet
         注意: 如果没有收到发送端的UDP报文,这一行代码将会阻塞当前线程的执行
         */
        socket.receive(packet);
        socket.close();

        System.out.println("接收到了数据: " + new String(data, 0, packet.getLength()));
    }
}

四、Java实现TCP程序

在TCP通信中,分为数据的发送端(客户端)和接收端(服务器),当建立连接成功后(三次握手),才可以进行数据的发送;

在Java中,提供了两个类用于实现TCP通信程序:

  • 1)客户端:java.net.Socket 类表示;用于与服务器端建立连接,向服务器端发送数据报文等;
  • 2)服务端:java.net.ServerSocket 类表示;用于与客户端的交互;

4.1 Socket

java.net.Sokcet用于封装一个TCP应用程序的客户端;

4.1.1 构造方法

  • public Socket(String host, int port) :创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为本机地址。

示例代码:

Socket client = new Socket("127.0.0.1", 6868);

4.1.2 成员方法

  • public InputStream getInputStream() : 返回此套接字的输入流。关闭生成的InputStream也将关闭相关的Socket。
  • public OutputStream getOutputStream() : 返回此套接字的输出流。关闭生成的OutputStream也将关闭相关的Socket。
  • public void close() :关闭此套接字。关闭此socket也将关闭相关的InputStream和OutputStream 。
  • public void shutdownOutput() : 禁用此套接字的输出流。任何先前写出的数据将被发送,随后终止输出流。

2.2 ServerSocket

ServerSocket类:这个类实现了服务器套接字,该对象等待通过网络的请求。

4.2.1 构造方法

  • public ServerSocket(int port) :使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。

构造举例,代码如下:

ServerSocket server = new ServerSocket(6666);

4.2.2 成员方法

  • public Socket accept() :监听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。

4.3 设计TCP应用程序

  • 客户端代码:
package com.dfbz.demo01_实现TCP;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_客户端 {
    public static void main(String[] args) throws Exception {

        // 创建一个客户端
        Socket socket = new Socket("localhost", 6666);

        // 获取与服务器的输入流(用于读取服务器的数据)
        InputStream is = socket.getInputStream();

        // 获取与服务器的输出流(用于向服务器写出数据)
        OutputStream os = socket.getOutputStream();

        // 发送数据到服务器
        os.write("你好呀~!在吗?".getBytes());

        byte[] data = new byte[1024];

        /*
         读取服务器的数据
         注意: 如果没有接收到服务器的数据,这一行代码将会阻塞当前线程的执行
         */
        int len = is.read(data);
        System.out.println("接收到来自服务器的信息【" + new String(data, 0, len) + "】");

        os.write("在干嘛?".getBytes());

        /*
         读取服务器的数据
         注意: 如果没有接收到服务器的数据,这一行代码将会阻塞当前线程的执行
         */
        len = is.read(data);
        System.out.println("接收到来自服务器的信息【" + new String(data, 0, len) + "】");

        socket.close();
    }
}
  • 服务端代码:
package com.dfbz.demo01_实现TCP;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_服务端 {
    public static void main(String[] args) throws Exception {
        // 创建了一个服务器
        ServerSocket serverSocket = new ServerSocket(6666);

        /*
         接收一个客户端
         注意: 如果没有客户端来连接服务器,这一行代码将会阻塞当前线程的执行
         */
        Socket client = serverSocket.accept();

        // 获取与客户端的输入流(用于读取客户端的数据)
        InputStream is = client.getInputStream();

        // 获取与客户端的输出流(用于向客户端写出数据)
        OutputStream os = client.getOutputStream();

        byte[] data = new byte[1024];

        /*
        从客户端读取数据
        注意: 如果没有接收到客户端的数据,这一行代码将会阻塞当前线程的执行
         */
        int len = is.read(data);
        System.out.println("接收到来自客户端的信息【" + new String(data, 0, len) + "】");

        os.write("在哦!".getBytes());

        /*
        从客户端读取数据
        注意: 如果没有接收到客户端的数据,这一行代码将会阻塞当前线程的执行
         */
        len = is.read(data);
        System.out.println("接收到来自客户端的信息【" + new String(data, 0, len) + "】");

        os.write("在吃饭呢!".getBytes());

        // 关闭资源
        client.close();
        serverSocket.close();
    }
}

上述程序中,发送内容都是写死在代码中,我们使用Scanner来接受键盘录入的数据进行发送;

  • 改造客户端程序:
package com.dfbz.demo02_使用Scanner改造;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_客户端 {
    public static void main(String[] args) throws Exception {

        // 创建一个客户端,并且去连接服务器
        Socket socket = new Socket("localhost", 7777);

        // 获取与服务器的输入/输出流(用于读取服务器的数据/向服务器写出数据)
        InputStream is = socket.getInputStream();
        OutputStream out = socket.getOutputStream();

        Scanner scanner = new Scanner(System.in);
        byte[] data = new byte[1024];

        while (true) {
            String str = scanner.nextLine();

            // 向服务器写出数据
            out.write(str.getBytes());

            // 读取服务器的信息
            int len = is.read(data);            // 这一句代码会造成阻塞,如果服务器没有向客户端写出数据,那么这一句代码阻塞
            System.out.println("来自服务器的信息【" + new String(data, 0, len) + "】");
        }

    }
}
  • 改造服务端程序:
package com.dfbz.demo02_使用Scanner改造;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_服务端 {
    public static void main(String[] args) throws Exception {
        // 创建一个服务器
        ServerSocket serverSocket = new ServerSocket(7777);

        // 接收到一个客户端(这一段代码会造成阻塞,如果没有客户端来连接服务器,那么代码会一直阻塞在这里)
        Socket client = serverSocket.accept();

        // 获取这个客户端的ip地址
        String hostAddress = client.getInetAddress().getHostAddress();

        // 获取这个客户端的端口
        int port = client.getPort();


        // 获取与这个客户端的输入/输出流(用于与这个客户端的读写操作)
        InputStream is = client.getInputStream();
        OutputStream os = client.getOutputStream();

        Scanner scanner = new Scanner(System.in);
        byte[] data = new byte[1024];

        while (true) {

            // 读取这个客户端的信息(如果客户端没有发送数据过来,这句代码会造成阻塞)
            int len = is.read(data);

            System.out.println("" + "来自客户端【" + hostAddress + "】" + "端口为【" + port + "】" + "的应用程序发送的消息【" + new String(data, 0, len) + "】");

            // 接收控制台的数据. 同样的,如果控制台没有输入数据,那么这一行代码也会阻塞当前线程的执行
            String str = scanner.nextLine();

            os.write(str.getBytes());
        }

    }
}

五、综合案例

5.1 图片上传案例

5.2.1 UDP实现图片上传

UDP图片上传流程:

需要注意的是:由于UDP的特点(面向无连接、不安全等),因此采用UDP协议上传的图片会造成数据丢失,图片失真、丢失像素等问题;

【发送端】

package com.dfbz.demo01_图片上传;

import java.io.FileInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_UDP_发送端 {
    public static void main(String[] args) throws Exception {
        // 创建一个套接字
        DatagramSocket socket = new DatagramSocket();

        // 从磁盘中读取文件
        FileInputStream fis = new FileInputStream("100.png");

        byte[] data = new byte[8192];

        // 创建一个UDP数据包
        DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("localhost"), 9999);

        int len;
        while ((len = fis.read(data)) != -1) {

            // 将从磁盘中读取到的数据设置到UDP报文中
            packet.setData(data);
            packet.setLength(len);

            // 发送UDP报文给接收端
            socket.send(packet);
        }

        // 发送一个空报文,作为结束标识
        packet.setLength(0);
        socket.send(packet);

        // 释放资源
        fis.close();
        socket.close();
    }
}

【接收端】

package com.dfbz.demo01_图片上传;

import java.io.FileOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_UDP_接收端 {

    public static void main(String[] args) throws Exception {

        // 创建一个套接字(用于接收UDP报文)
        DatagramSocket socket = new DatagramSocket(9999);

        byte[] data = new byte[8192];

        // 创建一个UDP数据包
        DatagramPacket packet = new DatagramPacket(data, data.length);

        // 创建一个文件输出流,将接收到的数据写入到这个文件中
        FileOutputStream fos = new FileOutputStream("udp.png");

        while (true) {
            // 接收UDP报文
            socket.receive(packet);

            // 获取报文数据长度
            int length = packet.getLength();

            if (length == 0) {
                // 说明读取到了末尾
                break;
            }
            fos.write(packet.getData(),0,length);
        }

        // 释放资源
        fos.close();
        socket.close();
    }
}

查看上传之前的原文件大小和上传之后的文件大小:

5.2.2 TCP实现图片上传

TCP图片上传流程:

1)客户端首先通过输入流将自己磁盘中的图片读取到内存中

2)客户端通过TCP连接的输出流,向服务器写出刚刚读取到的图片数据

3)服务器通过TCP连接的输入流,将客户端刚刚发送过来的图片数据读取到内存中

4)服务器通过输出流将内存中的数据写入到服务器的磁盘中

Tips:我们学习过程中,将服务器和客户端放在同一台机器。但实际开发中服务器和客户端不是在同一台机器,

【客户端代码实现】

package com.dfbz.demo02;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_Server {
    public static void main(String[] args) throws Exception {

        // 声明服务器
        ServerSocket serverSocket = new ServerSocket(8888);

        // 接收到一个客户端
        Socket client = serverSocket.accept();

        System.out.println(client.getInetAddress().getHostAddress() + "连接成功");

        // 读取客户端传递过来的数据
        InputStream is = client.getInputStream();

        // 随机生成一个文件名写出到磁盘
        FileOutputStream fos = new FileOutputStream(UUID.randomUUID().toString() + ".png");

        byte[] data = new byte[1024];

        int len;
        while ((len = is.read(data)) != -1) {

            // 写出到磁盘
            fos.write(data, 0, len);
        }

        // 释放资源
        fos.close();
        client.close();
    }
}

【服务端代码实现】

package com.dfbz.demo01_图片上传;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo04_TCP_服务端 {
    public static void main(String[] args) throws Exception {
        // 声明服务器
        ServerSocket serverSocket = new ServerSocket(8888);

        // 接收到一个客户端
        Socket client = serverSocket.accept();

        System.out.println(client.getInetAddress().getHostAddress() + "连接成功");

        // 读取客户端传递过来的数据
        InputStream is = client.getInputStream();

        // 随机生成一个文件名写出到磁盘
        FileOutputStream fos = new FileOutputStream("tcp.png");

        byte[] data = new byte[1024];
        int len;
        while ((len = is.read(data)) != -1) {

            // 写出到磁盘
            fos.write(data, 0, len);
        }

        // 释放资源
        fos.close();
        client.close();
    }
}

查看上传之前的原文件大小和上传之后的文件大小:

5.2.3 多线程改进TCP图片上传

实际开发中一个服务器对应N多个客户端,其他客户端均可以上传图片。我们的代码在同一时间只允许一个人上传图片,如果这个人上传的文件较大,那么势必会造成其他用户处于等待状态;针对这种情况我们可以使用多线程来解决。

服务器每次接受到一个客户端时,都开启一个线程来独立处理这个客户端的上传任务。这样在很多人同时来上传文件时,都可以一起上传。

  • 多线程改进服务器:
package com.dfbz.demo01_图片上传;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo05_TCP_服务端_多线程 {
    public static void main(String[] args) throws Exception {

        // 创建一个服务器
        ServerSocket serverSocket = new ServerSocket(8888);

        while (true) {
            // 每接收到一个客户端都创建一个新的线程为它服务
            Socket client = serverSocket.accept();

            System.out.println("客户端【" + client.getInetAddress().getHostAddress() + "】连接成功啦!");
            new Thread() {
                @Override
                public void run() {

                    try {
                        // 获取与客户端的输入流(用于读取客户端发送过来的字节)
                        InputStream is = client.getInputStream();

                        // 关联本地的一个文件(随机生成一个名称)
                        FileOutputStream fos = new FileOutputStream(UUID.randomUUID() + ".png");

                        byte[] data = new byte[8192];
                        int len;
                        while ((len = is.read(data)) != -1) {
                            fos.write(data, 0, len);
                        }

                        fos.close();
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    }
}

5.2 在线聊天案例

我们刚刚使用了TCP完成了聊天功能的编写;我们会发现我们的程序是由问题的,就是读写是串行的!

我们整个应用程序只有一个线程,那就mian线程,代码都是从上往下执行,如果main线程当前在读操作,那么就不能写。而且如果此时一方如果没有发送信息给另一方,那么另一方的read方法将会一直处于阻塞状态,代码不会往下执行;此时想往对方写出数据肯定是不行的;

我们利用多线程技术来改造我们之前的代码,让我们的代码既一直读,又可以一直写;

5.2.1 多线程UDP在线聊天案例

【多线程改进发送端】

package com.dfbz.demo02_多线程实现聊天案例;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_UDP_发送端 {
    public static void main(String[] args) throws Exception {
        new Thread(() -> {
            try {
                // 1. 创建一个套接字,用于发送数据到接收端
                DatagramSocket socket = new DatagramSocket();

                // 2. 创建一个UDP报文,用于封装要发送到接收端的数据(接收端的端口为9999)
                byte[] data = new byte[8192];
                DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getLocalHost(), 9999);

                // 3. 死循环发送信息给接收端
                Scanner scanner = new Scanner(System.in);
                while (true) {
                    // 接收键盘录入数据
                    String line = scanner.nextLine();

                    // 将数据设置到UDP报文中
                    packet.setData(line.getBytes());
                    packet.setLength(line.getBytes().length);

                    // 将此UDP报文发送给接收端
                    socket.send(packet);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }).start();


        new Thread(() -> {
            try {
                // 1. 创建一个套接字,用于接收来自接收端的数据(发送端的端口为7777)
                DatagramSocket socket = new DatagramSocket(7777);

                // 2. 创建一个UDP报文,用于接收来自接收端的数据
                byte[] data = new byte[8192];
                DatagramPacket packet = new DatagramPacket(data, data.length);

                // 3. 死循环接收来自接收端响应的信息
                while (true) {

                    // 接收来自接收端的UDP报文
                    socket.receive(packet);
                    data = packet.getData();
                    int len = packet.getLength();

                    System.out.println("来自接收端【" + packet.getAddress().getHostAddress() + "】的信息: 【" + new String(data, 0, len) + "】");
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

        }).start();
    }
}

【多线程改进接收端】

package com.dfbz.demo02_多线程实现聊天案例;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_UDP_接收端 {
    public static void main(String[] args) throws Exception {

        // 接收端的读线程
        new Thread(() -> {

            try {

                // 1. 创建一个套接字,用于接收来自发送端的数据(接收端的端口为9999)
                DatagramSocket socket = new DatagramSocket(9999);

                // 2. 创建一个UDP报文,用于接收来自发送端的数据
                byte[] data = new byte[8192];
                DatagramPacket packet = new DatagramPacket(data, data.length);

                // 3. 死循环接收来自发送端响应的信息
                while (true) {

                    // 接收来自发送端发送的UDP报文
                    socket.receive(packet);
                    data = packet.getData();
                    int len = packet.getLength();

                    System.out.println("来自发送端的【" + packet.getAddress().getHostAddress() + "】的信息: 【" + new String(data, 0, len) + "】");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        // 接收端的写线程
        new Thread(() -> {
            try {
                // 1. 创建一个套接字,用于发送数据给发送端
                DatagramSocket socket = new DatagramSocket();

                // 2. 创建一个UDP报文,用于封装要发送到发送端的数据(发送端的端口为7777)
                byte[] data = new byte[8192];
                DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getLocalHost(), 7777);

                // 3. 死循环发送信息给发送端
                Scanner scanner = new Scanner(System.in);
                while (true) {
                    // 接收键盘录入数据
                    String line = scanner.nextLine();

                    // 将数据设置到UDP报文中
                    packet.setData(line.getBytes());
                    packet.setLength(line.getBytes().length);

                    // 将此UDP报文发送给发送端
                    socket.send(packet);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }
}

5.2.2 多线程TCP在线聊天案例

【多线程改进客户端】

package com.dfbz.demo02_多线程实现聊天案例;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo03_TCP_客户端 {
    public static void main(String[] args) throws IOException {
        // 创建一个客户端,去连接服务器
        Socket socket = new Socket("127.0.0.1", 9999);

        // 获取与服务器的输入输出流
        InputStream is = socket.getInputStream();
        OutputStream os = socket.getOutputStream();

        // 读线程,专门用于读取服务器的信息
        new Thread() {

            @Override
            public void run() {
                try {
                    byte[] data = new byte[1024];

                    while (true) {
                        /*
                        一直死循环读取服务器发送过来的数据
                        如果服务器没有数据来也只是阻塞当前线程,并不会影响其他线程
                         */
                        int len = is.read(data);
                        System.out.println("接收到来自服务器的信息【" + new String(data, 0, len) + "】");
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        // 写线程,专门向服务器写出数据
        new Thread() {
            @Override
            public void run() {
                try {
                    Scanner scanner = new Scanner(System.in);
                    while (true) {
                        /*
                        一直死循环读取键盘录入的数据
                        如果键盘没有输入数据,也只是阻塞当前线程,并不会影响其他线程
                         */
                        String str = scanner.nextLine();
                        os.write(str.getBytes());
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}

【多线程改进服务端】

package com.dfbz.demo02_多线程实现聊天案例;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo04_TCP_服务端 {
    public static void main(String[] args) throws Exception {
        // 创建一台服务器
        ServerSocket serverSocket = new ServerSocket(9999);

        // 接收一个客户端
        Socket client = serverSocket.accept();

        // 获取与这个客户端的输入输出流
        InputStream is = client.getInputStream();
        OutputStream os = client.getOutputStream();

        // 获取客户端的IP地址
        String hostAddress = client.getInetAddress().getHostAddress();

        // 获取客户端的应用程序端口
        int port = client.getPort();
        System.out.println("有一个客户端来连接了,地址【" + hostAddress + "】,端口【" + port + "】");

        // 读线程
        new Thread() {
            @Override
            public void run() {
                try {
                    byte[] data = new byte[1024];

                    while (true) {
                          /*
                        一直死循环读取客户端发送过来的数据
                        如果客户端没有数据来也只是阻塞当前线程,并不会影响其他线程
                         */
                        int len = is.read(data);
                        System.out.println("接收到了来自客户端【" + hostAddress + "】的信息【" + new String(data, 0, len) + "】");
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        // 写线程
        new Thread() {
            @Override
            public void run() {
                try {
                    Scanner scanner = new Scanner(System.in);
                    while (true) {
                        /*
                        一直死循环读取键盘录入的数据
                        如果键盘没有输入数据,也只是阻塞当前线程,并不会影响其他线程
                         */
                        String str = scanner.nextLine();
                        os.write(str.getBytes());
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}
posted @ 2023-02-09 13:43  绿水长流*z  阅读(105)  评论(0编辑  收藏  举报