Java网络编程入门

Lou.Chen
大约 21 分钟

一、网络体系结构

通过网络发送数据是一项复杂的操作,必须仔细地协调网络的物理特性以及所发送数据的逻辑特征。通过网络将数据从一台主机发送到另外的主机,这个过程是通过计算机网络通信来完成。

网络通信的不同方面被分解为多个层,层与层之间用接口连接。通信的双方具有相同的层次,层次实现的功能由协议数据单元(PDU)来描述。不同系统中的同一层构成对等层,对等层之间通过对等层协议进行通信,理解批次定义好的规则和约定。每一层表示为物理硬件(即线缆和电流)与所传输信息之间的不同抽象层次。在理论上,每一层只与紧挨其上和其下的层对话。将网络分层,这样就可以修改甚至替换某一层的软件,只要层与层之间的接口保持不变,就不会影响到其他层。

计算机网络体系结构是计算机网络层次和协议的集合,网络体系结构对计算机网络实现的功能,以及网络协议、层次、接口和服务进行了描述,但并不涉及具体的实现。接口是同一节点内相邻层之间交换信息的连接处,也叫服务访问点(SAP)。

1、OSI七层模型 与 TCP/IP四层模型

我们现在互联网大都使用TCP/IP模型

①应用层

应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统DNS,支持万维网应用的 HTTP协议,支持电子邮件的 SMTP协议等等。我们把应用层交互的数据单元称为报文。

②运输层

运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。由于一台主机可同时运行多个线程,因此运输层有复用和分用的功能。所谓复用就是指多个应用层进程可同时使用下面运输层的服务,分用和复用相反,是运输层把收到的信息分别交付上面应用层中的相应进程。

运输层主要使用以下两种协议:

  1. 传输控制协议 TCP(Transmission Control Protocol)--提供面向连接的,可靠的数据传输服务。
  2. 用户数据协议 UDP(User Datagram Protocol)--提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。
③网络层

在 计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称 数据报

这里要注意:不要把运输层的“用户数据报 UDP ”和网络层的“ IP 数据报”弄混。另外,无论是哪一层的数据单元,都可笼统地用“分组”来表示。

这里强调指出,网络层中的“网络”二字已经不是我们通常谈到的具体网络,而是指计算机网络体系结构模型中第三层的名称.

互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Protocol)和许多路由选择协议,因此互联网的网络层也叫做网际层IP层

④数据链路层

数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。

在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。这样,数据链路层在收到一个帧后,就可从中提出数据部分,上交给网络层。 控制信息还使接收端能够检测到所收到的帧中有误差错。如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在链路层传输时出现差错(这就是说,数据链路层不仅要检错,而且还要纠错),那么就要采用可靠性传输协议来纠正出现的差错。这种方法会使链路层的协议复杂些。

⑤物理层

在物理层上所传送的数据单位是比特。 物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。 使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。

在互联网使用的各种协中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的TCP/IP并不一定单指TCP和IP这两个具体的协议,而往往表示互联网所使用的整个TCP/IP协议族。

二、传输层中TCP与UDP的区别

  • TCP(传输控制)协议

    • 使用TCP协议前,须先建立TCP连接,形成传输数据通道
    • 传输前,采用“三次握手”方式,点对点通信,是可靠的
    • TCP协议进行通信的两个应用进程:客户端、服务端
    • 在连接中可进行大数据量的传输
    • 传输完毕,需释放已建立的连接,采用“四次挥手"的方式,效率低
  • UDP(用户数据报)协议:

    • 将数据、源、目的封装成数据包,不需要建立连接
    • 每个数据报的大小限制在64K内
    • 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
    • 可以广播发送
    • 发送数据结束时无需释放资源,开销小,速度快

应用:

UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等

TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。

三、TCP连接三次握手四次挥手

TCP数据报格式:

报文段首部各个字段的含义:

  • 源端口号以及目的端口号:各占2个字节,端口是传输层和应用层的服务接口,用于寻找发送端和接收端的进程,一般来讲,通过端口号和IP地址,可以唯一确定一个TCP连接,在网络编程中,通常被称为一个socket接口。
  • 序号:seq(sequence)序号,占4个字节、32位。用来标识从TCP发送端向TCP接收端发送的数据字节流。发起方发送数据时对此进行标记。例如,一段报文的序号字段值是 301 ,而携带的数据共有100字段,显然下一个报文段(如果还有的话)的数据序号应该从401开始;
  • 确认序号:ack(acknowledge)序号,占4个字节、32位。包含发送确认的一端所期望收到的下一个序号。只有ACK标记位为1时,确认序号字段才有效,因此,确认序号应该是上次已经成功收到数据字节序号加1,即Ack=Seq + 1。
  • 数据偏移:占4个字节,用于指出TCP首部长度,若不存在选项,则这个值为20字节,数据偏移的最大值为60字节。
  • 保留字段占6位,暂时可忽略,值全为0。
  • 标志位,6个
    • URG(紧急):为1时表明紧急指针字段有效
    • ACK(确认):为1时表明确认号字段有效。TCP规定,在连接建立后所有报文的传输都必须把ACK置1;
    • PSH(推送):为1时接收方应尽快将这个报文段交给应用层
    • RST(复位):为1时表明TCP连接出现故障必须重建连接
    • SYN(同步):在连接建立时用来同步序号。当SYN=1,ACK=0,表明是连接请求报文,若同意连接,则响应报文中应该使SYN=1,ACK=1;
    • FIN(终止):为1时表明发送端数据发送完毕要求释放连接
  • **接收窗口:**占2个字节,用于流量控制和拥塞控制,表示当前接收缓冲区的大小。在计算机网络中,通常是用接收方的接收能力的大小来控制发送方的数据发送量。TCP连接的一端根据缓冲区大小确定自己的接收窗口值,告诉对方,使对方可以确定发送数据的字节数。
  • **校验和:**占2个字节,范围包括首部和数据两部分。
  • 选项是可选的,默认情况是不选。

①三次握手

最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。

  • 第一次握手(客户端发送请求):

    • 客户端向服务端发送SYN=1标志位的数据包,表明客户端发送一个需要建立连接的请求。这时服务端可以得出我能收到客户端的消息
  • 第二次握手(服务端回传确认):

    • 服务端收到客户端的连接请求后,向客户端发送SYN=1和ACK=1标志位的数据包,客户端确认收到服务端的消息。客户端在这时可以得出我发的消息服务端能够收到,服务端发的消息我也能收到
  • 第三次握手(客户端回传确认):

    • 客户端发送ACK=1确认标志位的数据包给服务端,服务端收到请求后,这时服务端可以得到我发送的消息客户端能够收到

注意

  • 握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。

  • **三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。**三次握手就能确认双发收发功能都正常,缺一不可

1)为什么TCP客户端最后还要发送一次确认呢?

主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。

​ 如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。

​ 如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

②四次挥手

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。

  • 第一次挥手:
    • 客户端向服务端发送一个FIN=1的标志位的数据包,表示客户端想要断开连接。此时服务端收到这条请求,但是不一定能够断开请求,可能客户端发送的数据完毕,而我服务端的数据还正在传输,即服务端可能还未准备完毕。
  • 第二次挥手:
    • 服务端向客户端发送ACK=1标志位的确认报文段数据包,客户端收到消息后,表示服务端已经知道我要断开连接了,服务端正在做断开连接的工作
  • 第三次挥手:
    • 等待一段时间后,服务端再次向客户端发送FIN=1标志位的数据包,表示服务端的断开之前的工作准备完毕,可以断开了
  • 第四次挥手
    • 客户端发送ACK=1标志位的确认报文段的数据包,表示客户端知道服务端已经准备好了。服务端收确认消息后即可断开连接

③为什么连接的时候是三次握手,关闭的时候却是四次挥手?

​ 因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭socket,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文,我收到了”。只有等到服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送,故需要四步挥手。

④为什么客户端最后还要等待2MSL?

客户端收到后就给服务器一个回信,为了防止回信丢失,客户端就再等2MSL个时间,之所以是2个,是因为涉及到来回,第一个MSL中是回信在路上的最大时间,第二个MSL是万一回信没到服务端,服务端重发的FIN确认在路上的时间

https://www.cnblogs.com/swordfall/p/10781281.html#auto_id_7open in new window

https://blog.csdn.net/qzcsu/article/details/72861891open in new window

四、网络通信的三要素

1、IP地址

  • 唯一的标识 Internet 上的计算机(通信实体)
  • 本地回环地址(hostAddress):127.0.0.1 主机名(hostName):localhost
  • IP地址分类方式1:IPV4 和 IPV6
    • IPV4:4个字节组成,4个0-255。大概42亿,30亿都在北美,亚洲4亿。2011年初已 经用尽。以点分十进制表示,如192.168.0.1
    • IPV6:128位(16个字节),写成8个无符号整数,每个整数用四个十六进制位表示, 数之间用冒号(:)分开,如:3ffe:3201:1401:1280:c8ff:fe4d:db39:1984
  • IP地址分类方式2:公网地址(万维网使用)和私有地址(局域网使用)。192.168. 开头的就是私有址址,范围即为192.168.0.0--192.168.255.255,专门为组织机 构内部使用

2、端口号

  • 端口号标识正在计算机上运行的进程(程序)
  • 不同的进程有不同的端口号
  • 被规定为一个 16 位的整数 0~65535。
  • 端口分类:
    • 公认端口:0~1023。被预先定义的服务通信占用(如:HTTP占用端口 80,FTP占用端口21,Telnet占用端口23)
    • 注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat占 用端口8080,MySQL占用端口3306,Oracle占用端口1521等)。
    • 动态/私有端口:49152~65535。 、
  • 端口号与IP地址的组合得出一个网络套接字:Socket。

3、网络通信协议

计算机网络中实现通信必须有一些约定,即通信协议,对速率、传输代码、代 码结构、传输控制步骤、出错控制等制定标准。

  • 传输层协议中有两个非常重要的协议:

    • 传输控制协议TCP(Transmission Control Protocol)
    • 用户数据报协议UDP(User Datagram Protocol)。
  • TCP/IP 以其两个主要协议:传输控制协议(TCP)和网络互联协议(IP)而得 名,实际上是一组协议,包括多个具有不同功能且互为关联的协议。

  • IP(Internet Protocol)协议是网络层的主要协议,支持网间互连的数据通信

  • TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即 物理链路层、IP层、传输层和应用层

五、TCP网络编程

1、概述

TCP通信能实现两台计算机直接的交互,通信的两端,要严格区分客户端和服务端

两端通信时步骤:

①服务端程序首先启动,等待客户端的连接

②客户端主动连接服务器,连接成功才能进行通信。服务端不可以主动连接客户端

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

  • 客户端java.net.Socket类表示。创建Socket对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信
  • 服务端java.net.ServerSocket类表示。创建ServerSocekt对象。相当于开启一个服务,并等待客户端的连接

2、Socket

该类实现客户端套接字(也称为“套接字”)。 套接字是两台机器之间通讯的端点

  • 利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实 上的标准
  • 网络上具有唯一标识的IP地址和端口号组合在一起才能构成唯一能识别的标 识符套接字。
  • 通信的两端都要有Socket,是两台机器间通信的端点。
  • 网络通信其实就是Socket间的通信。
  • Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。
  • 一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。
  • Socket分类
    • 流套接字(stream socket):使用TCP提供可依赖的字节流服务
    • 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务
①构造方法
  • host: 服务器主机的名称或者IP地址
  • port :端口号

Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号。

②实例方法

OutputStream getOutputStream() 返回此套接字的输出流。(向服务端发送数据)

InputStream getInputStream() 返回此套接字的输入流。(接收服务端的数据)

void shutdownOutput() 禁用此套接字的输出流(加入终止符), 对于TCP套接字,任何先前写入的数据将被发送,随后是TCP的正常连接终止序列。 如果在套接字上调用shutdownOutput()之后写入套接字输出流,则流将抛出IOException。

void close() 关闭此套接字

3、ServerSocket

这个类实现了服务器套接字。 服务器套接字等待通过网络进入的请求。 它根据该请求执行一些操作,然后可能将结果返回给请求者。

①构造方法

ServerSocket(int port) 创建绑定到指定端口的服务器套接字。 (创建服务器,并指定端口)

②实例方法

Socket accept() 侦听要连接到此套接字并接受它。 (帧听客户端的请求,若帧听到则返回此客户端的Socket对象)

③通过accept获取的Socket对象

InputStream getInputStream() 返回此套接字的输入流。(接收客户端的数据)

OutputStream getOutputStream() 返回此套接字的输出流。(向客户端发送数据)

4、实验步骤

1)服务端
public class TCPServer {
    public static void main(String[] args) throws IOException {
        //1、创建服务器,并指定端口
        ServerSocket serverSocket = new ServerSocket(9001);
        System.out.println("服务端已启动...正在监听:");
        //2、帧听客户端的请求,若帧听到则返回此客户端的Socket对象
        Socket accept = serverSocket.accept();
        //3、使用Socket中的getInputStream方法获取网络字节输入流对象InputStream(接收客户端发送的数据)
        InputStream is = accept.getInputStream();
        //4、读取客户端发送的数据
        byte[] bytes = new byte[1024];
        //注意若采取while循环的方式读取,那么最后一次读取则将未读取到任何值,则会阻塞
        int read = is.read(bytes);
        System.out.println("接收客服端的数据:");
        System.out.println(new String(bytes,0,read));
        //5、使用Socket中的getOutputStream方法获取网络字节输出流对象OutputStream(向客户端发送数据)
        OutputStream os = accept.getOutputStream();
        //6、向客户端发送数据
        os.write("服务端已接收到数据...谢谢!".getBytes());
        os.flush();
        //7、关闭套接字
        accept.close();
        serverSocket.close();
    }
}
2)客户端
public class TCPClient {
    public static void main(String[] args) throws IOException {
        //1、绑定服务端的地址并指定端口,获得socket对象
        Socket socket = new Socket("127.0.0.1", 9001);
        //2、通过socket对象中的getOutputStream()方法获取网络字节数出流对象OutputStream(向服务端发送数据)
        OutputStream os = socket.getOutputStream();
        //3、向服务端发送数据
        os.write("我向服务端发送了一条数据...".getBytes());
        //4、使用Socket对象中的getInputStream()方法获取网络字节输入流对象InputStream
        InputStream is = socket.getInputStream();
        //5、接收服务端的数据
        int len;
        byte[] bytes = new byte[1024];
        System.out.println("客户端接收的数据:");
        while ((len = is.read(bytes))!= -1) {
            System.out.println(len);
            System.out.println(new String(bytes,0,len));
        }
        //6、关闭套接字,释放资源
        socket.close();
    }
}

六、文件上传案例

1、服务端

public class TCPServer {
    public static void main(String[] args) throws IOException {
        //1、创建客户端
        ServerSocket serverSocket=new ServerSocket(9001);
        //创建线程池保持并发率
        ExecutorService pool = Executors.newCachedThreadPool();
        //一直保持客户端的监听
        while (true) {
            Socket accept = serverSocket.accept();
            pool.execute(()-> {
                try {
                    //2、监听客户端
                    //3、获得客户的输入流
                    InputStream is = accept.getInputStream();
                    File file = new File("F:/upload");
                    //4、判断服务器上传的指定目录是否存在
                    if (!file.exists()) {
                        file.mkdir();
                    }
                    //名称随机
                    String filename = System.currentTimeMillis() + new Random().nextInt(100) + ".jpg";
                    //5、创建输出流向指定目录服务端写数据
                    FileOutputStream fos = new FileOutputStream(file + "/" + filename);
                    //、读取客户端数据
                    int len;
                    byte[] bytes = new byte[1024];
                    System.out.println("ssssss1111");
                    //注意:socket中获取的InputStream中read()方法本身是一个阻塞方法,若读取到的数据默认没有终止符,则会一直阻塞。
                    //所以我们客户端在上传文件时需要手动添加终止符
                    while ((len = is.read(bytes)) != -1) {
                        //写入到目录
                        fos.write(bytes, 0, len);
                    }
                    System.out.println("ssssss22222");
                    OutputStream os = accept.getOutputStream();
                    os.write("上传文件成功".getBytes());
                    fos.close();
                    accept.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        //始终监听客户端, 不用关闭服务器
//        serverSocket.close();
    }
}

2、客户端

public class TCPCilent {
    public static void main(String[] args) throws IOException {
        //1、获得数据源(即上传的文件)
        FileInputStream fis=new FileInputStream("F:/test/1.jpg");
        //2、绑定服务器
        Socket socket = new Socket("127.0.0.1", 9001);
        //3、获得字节输出流 (上传文件)
        OutputStream os = socket.getOutputStream();
        int len;
        byte[] bytes = new byte[1024];
        while ((len = fis.read(bytes))!=-1) {
            //注意:我们客户端在向客户端写入数据时,并没有在数据默认添加终止符。
            os.write(bytes, 0,len);
        }
        //手动添加终止符
        socket.shutdownOutput();
        System.out.println("ccccc1111");
        //4、读取服务端的数据
        InputStream is = socket.getInputStream();
        while ((len = is.read(bytes)) != -1) {
            System.out.println(new String(bytes,0,len));
        }
        System.out.println("ccccc22222");
        fis.close();
        socket.close();
    }
}