16【TCP、UDP、网络通信】


上一篇15【IO流增强】


下一篇17【测试单元、反射、注解、Lombok插件】

目录【JavaSE零基础系列教程目录】


文章目录

  • 16【TCP、UDP、网络通信】
  • 一、基于网络编程
    • 1.1 网络编程概述
    • 1.2 网络分层
      • 1.2.1 OSI参考模型
      • 1.2.2 TCP/IP参考模型
    • 1.3 网络名词
      • 1.3.1 IP地址
      • 1.3.2 端口号
    • 1.4 网络协议
      • 1.4.1 UDP
        • 1)UDP传输过程
        • 2)UDP报文格式
        • 3)UDP总结
      • 1.4.2 TCP
        • 1)TCP报文格式
        • 2)三次握手
        • 3)四次挥手
        • 4)TCP总结
  • 二、Java实现UDP应用程序
    • 2.1 InetAddress类
    • 2.2 DatagramPacket类
      • 2.2.1 构造方法
      • 2.2.2 常用方法
    • 2.2 DatagramSocket类
      • 2.2.1 构造方法
      • 2.2.2 常用方法
    • 2.3 UDP实现数据发送与接收
  • 三、Java实现TCP程序
    • 2.1 Socket
      • 2.1.1 构造方法
      • 2.1.2 成员方法
    • 2.2 ServerSocket
      • 2.2.1 构造方法
      • 2.2.2 成员方法
    • 2.3 设计TCP应用程序
  • 四、综合案例
    • 4.1 TCP在线聊天案例
    • 4.2 TCP图片上传案例
      • 4.2.1 简单图片上传
      • 4.2.2 使用多线程改进


16【TCP、UDP、网络通信】

一、基于网络编程

1.1 网络编程概述

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

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

1.2 网络分层

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

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

16【TCP、UDP、网络通信】_第1张图片

1.2.1 OSI参考模型

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

16【TCP、UDP、网络通信】_第2张图片

  • 物理层:为数据端设备提供原始比特流的传输的通路;网络通信的数据传输介质,由电缆与设备共同构成。常见:中继器,集线器(HUB)、网线、RJ-45标准等
  • 数据链路层:在通信的实体间建立数据链路连接;将数据分帧,并处理流控制、物理地址寻址、重发等。常见:网卡,网桥,二层交换机等
  • 网络层:为数据在结点之间传输创建逻辑链路,并分组转发数据;对子网间的数据包进行路由选择。常见:路由器、多层交换机、防火墙、IP、IPX、RIP、OSPF
  • 传输层:提供应用进程之间的逻辑通信;建立连接,处理数据包错误、数据包次序。常见:TCP、UDP、SPX、进程、端口(socket)
  • 会话层:建立端连接并提供访问验证和会话管理;为数据在结点之间传输创建逻辑链路,并分组转发数据;使用校验点可使会话在通信失效时从校验点恢复通信。常见:服务器验证用户登录、断点续传
  • 表示层:提供数据格式转换服务;解密与加密,图片解码和编码、数据的压缩和解压缩。常见:URL加密、口令加密、图片编解码
  • 应用层:访问网络服务的接口;为操作系统或网络应用程序提供访问网络服务的接口。常见:Telnet、FTP、 HTTP、 SNMP、DNS等

1.2.2 TCP/IP参考模型

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

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

16【TCP、UDP、网络通信】_第3张图片

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,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。

16【TCP、UDP、网络通信】_第4张图片

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

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

1.3.2 端口号

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

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

1.4 网络协议

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

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

16【TCP、UDP、网络通信】_第5张图片

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

1.4.1 UDP

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

1)UDP传输过程

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

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

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

16【TCP、UDP、网络通信】_第6张图片

2)UDP报文格式

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

16【TCP、UDP、网络通信】_第7张图片

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

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

3)UDP总结

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

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

1.4.2 TCP

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

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

16【TCP、UDP、网络通信】_第8张图片

1)TCP报文格式

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

16【TCP、UDP、网络通信】_第9张图片

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

16【TCP、UDP、网络通信】_第10张图片

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

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

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

A向B发送数据:

16【TCP、UDP、网络通信】_第11张图片

B向A发送数据:

16【TCP、UDP、网络通信】_第12张图片

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

  • 本次的序列号:上次的确认号
  • 本次的确认号:上次的序列号+1
  • 数据偏移:占4个比特,用来指出数据载荷部分的起始处距离报文的起始处有多远;也就是TCP首部的长度。需要注意的是数据偏移以4个字节为单位;

如图:

16【TCP、UDP、网络通信】_第13张图片

  • 保留字段:占6个比特,保留为今后使用,但目前为0;

  • 窗口: 占2个字节,16个比特;用于流量控制和拥塞控制,表示当前接收缓冲区的大小。在计算机网络中,通常是用接收方的接收能力的大小来控制发送方的数据发送量。TCP连接的一端根据缓冲区大小确定自己的接收窗口值,告诉对方,使对方可以确定发送数据的字节数。

  • 校验和:占2个字节,16比特;检查报文的首部和数据载荷两部分,底层依赖于具体的校验算法;

  • 紧急指针:占2个字节,16比特;用来指明紧急数据的长度;当发送端有紧急数据时,可将紧急数据插队到发送缓存的最前面,并立刻封装到一个TCP报文段中进行发送。紧急指针会指出本报文段数据载荷部分包含了多长的紧急数据,紧急数据之后是普通数据

  • 选项:附加一些额外的首部信息;

  • 填充:由于选项的长度可变,因此用来填充的确认报文首部能被4整除(因为数据偏移字段,也就是首部长度字段,是以4字节为单位的);

  • 标志位

    • ACK(确认):取值为1时确认号字段才有效;取值为0时确认号字段无效,一般情况下都为1;
    • SYN(同步):在连接建立时用来同步序号
    • FIN(终止):为1时表明发送端数据发送完毕要求释放连接
    • RST(复位):用于复位TCP连接,值为1时说明连接出现了异常,必须释放连接,然后再重新建立连接,有时RST置1还用来拒绝一个非法的报文段或拒绝打开一个TCP连接;
    • PSH(推送):为1时接收方应尽快将这个报文交给应用层,而不必等到接受缓存都填满后再向上交付
    • URG(紧急):为1时表明紧急指针字段有效,取值为0时紧急字段无效;

2)三次握手

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

简单的过程如下图所示:

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

16【TCP、UDP、网络通信】_第14张图片

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

原理图如下:

16【TCP、UDP、网络通信】_第15张图片

  • 1)第一次握手:首先A使用TCP连接向B发送报文(此报文不携带载荷,只有首部),报文中的SYN记为1(如果发送失败,重新发送记为2),序号记为x

  • 2)第二次握手:B接受到A发送的报文后,给A发送一个确认报文(该报文也仅有首部),报文中SYN和ACK都取值为1,用于标志这是一个确认报文段;同时此次响应的序号记为y,确认号为上一次序号x加1的值,表示已经成功接收到x+1之前的所有内容,下一次开始接受x+1之后的数据(包含x+1的数据);

  • 3)第三次握手:A再次向B发送TCP确认,序号为x+1,代表发送序号x之后的数据;确认号为y+1,代表已经接收了来自B的响应的全部数据;

3)四次挥手

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

原理图如下:

16【TCP、UDP、网络通信】_第16张图片

  • 1)A向B发送连接释放请求,标记FIN=1(表示需要断开连接);序号记为u(u为之前已发送的最后一个字节数据的序号+1),确认号记为v(v为之前已接受的最后一个字节数据的序号+1)
  • 2)B收到A发送的断开连接请求后开始响应,标记序号为v,确认号为u+1(代表已经成功接收A发送的数据,接下来开始接受u+1及之后序号的数据)
  • 3)主机B如果仍有数据需要传输依旧可以传输,如果B没有数据需要发送时,需给A发送连接释放报文。标记FIN=1,序号为w(同理,此时w是B之前已发送最后一个字节数的序号+1),确认号为u+1;
  • 4)A接收到B发送的连接释放报文后,最终给B发送一个TCP确认报文,代表确定接受到B需要连接断开。序号为u+1,确认号为w+1(代表B发送的数据以及全部接受完毕)

4)TCP总结

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

二、Java实现UDP应用程序

2.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;

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

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

        // 根据域名获取InetAddress对象(获取本机对象)
        InetAddress t1 = InetAddress.getByName("localhost");
        System.out.println(t1.getHostName());           // localhost
        System.out.println(t1.getHostAddress());        // 127.0.0.1

        System.out.println("------------------------");
        // 根据域名获取InetAddress对象(获取百度的InetAddress对象)
        InetAddress t2 = InetAddress.getByName("www.baidu.com");
        System.out.println(t2.getHostName());           // www.baidu.com
        System.out.println(t2.getHostAddress());        // 163.177.151.109
    }
}

2.2 DatagramPacket类

java.net.DatagramPacket类用于封装一个UDP数据报文

2.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;

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

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) throws UnknownHostException {
        byte[] data1 = "hello~".getBytes();
        DatagramPacket packet1 = new DatagramPacket(data1, data1.length);

        byte[] data2 = "你好".getBytes();
        InetAddress address = InetAddress.getByName("localhost");
        DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, 6868);
    }
}

2.2.2 常用方法

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

示例代码:

package com.dfbz.demo01;

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

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo03 {
    public static void main(String[] args) throws UnknownHostException {
        byte[] data = "hello".getBytes();
        DatagramPacket packet = new DatagramPacket(
                data,
                data.length,
                InetAddress.getByName("localhost"),
                6868
        );

        System.out.println(packet.getLength());         // 5
        System.out.println(packet.getPort());           // 6868
        System.out.println(new String(packet.getData()));   // hello
    }
}

2.2 DatagramSocket类

java.net.DatagramSocket类用于描述一个UDP发送端或接收端;

2.2.1 构造方法

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

示例代码:

DatagramSocket socket = new DatagramSocket(6969);

2.2.2 常用方法

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

2.3 UDP实现数据发送与接收

  • 发送端:
package com.dfbz.demo01;

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 {
        String str = "你好";

        // 准备一个UDP数据包
        DatagramPacket packet = new DatagramPacket(
                str.getBytes(),
                str.getBytes().length,
                InetAddress.getLocalHost(),
                6868);

        // 套接字
        DatagramSocket socket = new DatagramSocket();

        // 发送数据包
        socket.send(packet);
        socket.close();
    }
}
  • 接收端:
package com.dfbz.demo01;

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

/**
 * @author lscl
 * @version 1.0
 * @intro: 接收端
 */
public class Demo05 {
    public static void main(String[] args) throws Exception {
        DatagramSocket socket = new DatagramSocket(666);

        byte[] bytes = new byte[1024];
        DatagramPacket packet = new DatagramPacket(bytes, bytes.length);

        //接受数据字节的长度
        System.out.println("接收端端:");

        // 接受一个UDP数据包
        socket.receive(packet);

        int len = packet.getLength();
        System.out.println("已经接收到:" + len + "个字节");

        // 转换为字符串打印
        System.out.println(new String(bytes, 0, len));
        socket.close();
    }
}

三、Java实现TCP程序

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

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

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

  • 2)服务端:java.net.ServerSocket 类表示;用于与客户端的交互;

2.1 Socket

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

2.1.1 构造方法

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

示例代码:

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

2.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类:这个类实现了服务器套接字,该对象等待通过网络的请求。

2.2.1 构造方法

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

构造举例,代码如下:

ServerSocket server = new ServerSocket(6666);

2.2.2 成员方法

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

2.3 设计TCP应用程序

  • 客户端代码:
package com.dfbz.demo01;

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

/**
 * @author lscl
 * @version 1.0
 * @intro: 客户端程序
 */
public class Demo01_Client {
    public static void main(String[] args) throws IOException {
        // 创建一个客户端,指定要连接服务器的IP与端口
        Socket socket = new Socket("127.0.0.1", 6969);

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

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

        // 向服务器发送数据
        os.write("在吗?".getBytes());

        // 准备一个字节数组用于接收数据
        byte[] bytes = new byte[1024];

        // 读取服务器发送过来的数据(若服务器一直未发送数据,则程序阻塞在此)
        int len = is.read(bytes);

        System.out.println(new String(bytes, 0, len));

        // 向服务器写出数据
        os.write("买糖".getBytes());

        // 读取服务器的数据
        len = is.read(bytes);

        System.out.println(new String(bytes, 0, len));

        // 释放连接资源
        socket.close();
    }
}
  • 服务端代码:
package com.dfbz.demo01;

import java.io.IOException;
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_Server {
    public static void main(String[] args) throws IOException {

        // 创建一个服务器,并指定该TCP程序的端口(IP地址就是本机)
        ServerSocket serverSocket=new ServerSocket(6969);

        System.out.println("等待客户端: ");

        // 接收一个客户端(若没有客户端来连接服务器,则程序阻塞在此)
        Socket client = serverSocket.accept();

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

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

        byte[] bytes=new byte[1024];

        // 读取客户端发送过来的数据
        int len = is.read(bytes);

        System.out.println(new String(bytes,0,len));

        // 向客户端写出数据
        os.write("在哦!亲~,想买点什么?".getBytes());

        // 读取客户端的数据
        len = is.read(bytes);

        System.out.println(new String(bytes,0,len));

        // 向客户端写出数据
        os.write("糖没了,不卖!".getBytes());

        client.close();
        serverSocket.close();
    }
}

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

  • 改造客户端程序:
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 Demo01_Client {
    public static void main(String[] args) throws IOException {
        // 创建一个客户端,指定要连接服务器的IP与端口
        Socket socket = new Socket("127.0.0.1", 6969);

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

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

        // 获取scanner扫描器
        Scanner scanner = new Scanner(System.in);

        // 死循环进行发送
        while (true) {

            System.out.println("请输入要发送给服务器的信息: ");
            String sendInfo = scanner.next();

            // 如果输入end代表程序结束
            if ("end".equals(sendInfo)) {
                System.out.println("程序结束...");
                break;
            }
            // 向服务器发送数据
            os.write(sendInfo.getBytes());

            // 准备一个字节数组用于接收数据
            byte[] bytes = new byte[1024];

            // 读取服务器发送过来的数据(若服务器一直未发送数据,则程序阻塞在此)
            int len = is.read(bytes);
            System.out.println("接收到来自服务器" + socket.getInetAddress().getHostAddress() + "的信息: " + new String(bytes, 0, len));
        }

        // 释放连接资源
        socket.close();
    }
}
  • 改造服务端程序:
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 Demo02_Server {
    public static void main(String[] args) throws IOException {

        // 创建一个服务器,并指定该TCP程序的端口(IP地址就是本机)
        ServerSocket serverSocket = new ServerSocket(6969);

        System.out.println("等待客户端: ");

        // 接收一个客户端(若没有客户端来连接服务器,则程序阻塞在此)
        Socket client = serverSocket.accept();

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

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

        Scanner scanner = new Scanner(System.in);

        while (true) {
            byte[] bytes = new byte[1024];

            // 读取客户端发送过来的数据
            int len = is.read(bytes);

            String receiveInfo = new String(bytes, 0, len);

            // 如果客户端发送过来的是end则程序结束
            if ("end".equals(receiveInfo)) {
                System.out.println("程序结束...");
                break;
            }
            System.out.println("接受到来自客户端" + client.getInetAddress().getHostAddress() + "的信息: " + receiveInfo);

            String sendInfo = scanner.next();
            // 向客户端写出数据
            os.write(sendInfo.getBytes());
        }
        client.close();
        serverSocket.close();
    }
}

四、综合案例

4.1 TCP在线聊天案例

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

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

16【TCP、UDP、网络通信】_第17张图片

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

  • 多线程改进客户端:
package com.dfbz.demo01;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro: 多线程实现客户端
 */
public class Demo01_Client {

    public static void main(String[] args) throws Exception {
        // 获取到一个新的客户端
        Socket socket = new Socket("127.0.0.1", 6969);

        // 读取客户端数据
        InputStream is = socket.getInputStream();

        // 缓冲字符输入流(读取客户端数据更加方便)
        BufferedReader br = new BufferedReader(new InputStreamReader(is));

        // 读线程
        new Thread() {
            @Override
            public void run() {
                try {
                    String info;
                    while (true) {

                        // 死循环读取客户端的信息
                        info = br.readLine();

                        if (info.equals("end")) {

                            socket.close();

                            // 退出应用程序
                            System.exit(0);
                            System.out.println("bye bye....");
                            break;
                        } else {
                            System.out.println("接收到: " + socket.getInetAddress().getHostAddress() + "来自的消息: " + info);
                        }
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();


        // 给客户端写回数据
        OutputStream os = socket.getOutputStream();

        // 缓冲字符输出流(往客户端写数据更加方便)
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));

        // 写线程
        new Thread() {
            @Override
            public void run() {
                try {

                    Scanner scanner = new Scanner(System.in);
                    String info;

                    while (true) {
                        // 获取键盘输入的信息
                        info = scanner.nextLine();

                        // 往客户端写数据
                        bw.write(info);
                        bw.newLine();
                        bw.flush();
                    }

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

            }
        }.start();
    }

}
  • 多线程改进服务器:
package com.dfbz.demo01;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro: 多线程实现服务器
 */
public class Demo02_Server {
    public static void main(String[] args) throws Exception {
        // 创建一台服务器
        ServerSocket serverSocket = new ServerSocket(6969);

        while (true) {
            // 获取到一个新的客户端
            Socket socket = serverSocket.accept();

            System.out.println(socket.getInetAddress().getHostAddress() + "已与您连接成功");

            // 读取客户端数据
            InputStream is = socket.getInputStream();

            // 缓冲字符输入流(读取客户端数据更加方便)
            BufferedReader br = new BufferedReader(new InputStreamReader(is));

            // 读线程
            new Thread() {
                @Override
                public void run() {
                    try {
                        String info;
                        while (true) {

                            // 死循环读取客户端的信息
                            info = br.readLine();

                            if (info.equals("end")) {

                                // 关闭与客户端的连接
                                socket.close();
                                System.out.println(socket.getInetAddress().getHostAddress() + "已与您断开连接: ");
                                break;
                            } else {
                                System.out.println("接收到: " + socket.getInetAddress().getHostAddress() + "来自的消息: " + info);
                            }
                        }

                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }.start();


            // 给客户端写回数据
            OutputStream os = socket.getOutputStream();

            // 缓冲字符输出流(往客户端写数据更加方便)
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));

            // 写线程
            new Thread() {
                @Override
                public void run() {
                    try {

                        Scanner scanner = new Scanner(System.in);
                        String info;

                        while (true) {
                            // 获取键盘输入的信息
                            info = scanner.nextLine();

                            // 往客户端写数据
                            bw.write(info);
                            bw.newLine();
                            bw.flush();
                        }

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

                }
            }.start();
        }

    }
}

4.2 TCP图片上传案例

4.2.1 简单图片上传

图片上传流程:

16【TCP、UDP、网络通信】_第18张图片

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.demo02;

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

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_Client {
    public static void main(String[] args) throws Exception{
        Socket socket = new Socket("localhost", 8888);

        // 读取磁盘中的图片
        FileInputStream fis = new FileInputStream("C:\\Users\\Horizon\\Desktop\\001.png");

        // 往服务器传递数据
        OutputStream os = socket.getOutputStream();

        byte[] data=new byte[1024];

        int len;

        while ((len=fis.read(data))!=-1){
            // 写出到服务器端
            os.write(data,0,len);
        }

        fis.close();
        socket.close();
    }
}

4.2.2 使用多线程改进

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

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

  • 多线程改进服务器:
package com.dfbz.demo03;

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

/**
 * @author lscl
 * @version 1.0
 * @intro: 多线程改进图片上传服务器
 */
public class Demo01_Server {
    public static void main(String[] args) throws Exception {

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

        while (true) {
            // 接收到一个客户端
            Socket socket = serverSocket.accept();

            // 每接收到一个客户端都开启一个独立的线程为该客户端提供服务
            new Thread(){
                @Override
                public void run() {
                    System.out.println(socket.getInetAddress().getHostAddress() + "连接成功");

                    try {
                        // 读取客户端传递过来的数据
                        InputStream is = socket.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);
                        }

                        System.out.println("文件上传成功!");
                        // 释放资源
                        fos.close();
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    }
}

上一篇15【转换流、缓冲流、序列流、打印流】


下一篇17【测试单元、反射、注解、Lombok插件】

目录【JavaSE零基础系列教程目录】


你可能感兴趣的:(#,《JavaSE系列》,UDP,TCP,Socket,网络编程,三次握手)