「课件」原创 => Java's 网络编程【转载请标明出处】
第一章·网络编程前置知识
1.1 OSI 参考模型
- 物理层: 负责传输比特流,处理物理连接细节,如电压和光的调制。物理层 就是 硬件层面,比如说我们连接的光缆、总线之类的,就会传递电信号,电信号高电压和低电压就是 1 和 0 的表示,这不就构成二进制了嘛
- 数据链路层: 处理帧的传输,负责错误检测和纠正,以及设备的物理寻址。
数据链路层就好比是运输体系中的快递服务。想象一下你要寄一封信给你的朋友。在这个过程中,有两个主要的问题需要解决:
- 如何确保信件的安全传输?
就像快递服务需要防止包裹在运输途中受损一样,数据链路层负责确保在网络中传输的数据的完整性。这意味着它会 检测并纠正在传输过程中可能发生的错误,就像快递服务可能会用防护材料包裹,包裹以保护其内容一样。
- 如何确定信件是寄给正确的人?
数据链路层还涉及到地址的问题。在计算机网络中,每个设备都有一个唯一的物理地址,称为 MAC 地址。这就好比快递服务会使用地址标签确保包裹被送到正确的目的地一样。
数据链路层的任务就是在网络中提供可靠的、有序的、错误检测和纠正的数据传输。就像快递员在传递包裹时负责包裹的完整性和准确的递送一样。
- 网络层: 处理数据包的路由选择,提供逻辑寻址和跨网络通信。
网络层就像是运输体系中的交通规划部门,负责管理不同地址之间的交通和路由
在这个过程中,有两个关键问题需要解决:
- 如何决定信件的最佳路径?
就像交通规划部门需要决定汽车、公交车或飞机等交通工具的最佳路线一样,网络层负责确定数据在网络中传输的路径。这就是所谓的路由,确保信息能够从发送者到达接收者。
- 如何处理不同城市之间的通信?
在计算机网络中,我们可能有不同的子网络或不同的网络。网络层通过使用 IP 地址来识别不同的设备和网络,就像城市地址一样。
所以,网络层的任务是提供端到端的通信,确保数据从源头传输到目的地,就像交通规划部门确保人们能够从一个城市到达另一个城市一样。
- 传输层: 提供端到端的通信,负责数据流控制、错误检测和纠正。
传输层就像是邮递员一样,负责确保我们发送的信件能够准确无误地到达目的地。这一层主要有两个关键的任务:保证信件不丢失和控制发送的速度。
- 保证信件不丢失(Reliability):传输层会采用一些特殊的方法,比如确认收到和重新发送,来确保我们发送的信息不会在途中丢失。就好比你寄一封信,如果对方没收到,你会得到一个回执,然后你会再寄一次。
- 控制发送的速度(Flow Control):传输层还会控制发送信息的速度,防止发送的太快导致接收方处理不过来。这就好比你在一封信里告诉对方你每次最多能接受多少封信,以确保你不会被信息淹没。就是上一封信,可能还没 读完呢,你这边咔咔的又来好几封。。
总的来说,传输层就是一个保障我们发送的信息不丢失、有序到达,并且不会淹没接收方的一位“邮递员”。这里有两种主要的“邮递方式”:一种是像快递员一样每次确认并逐一送达(TCP),另一种是直接寄出去,不管它能否准确到达(UDP)。不同的场景需要选择不同的方式,就像有时你会选择用快递,有时候用平信(只管邮寄,不管其它)一样。
- 会话层: 管理不同设备之间的对话,包括建立、维护和结束对话。
会话层就好比你和朋友聊天的过程。当你与朋友进行通信时,会话层负责确保你们的对话是有序、可靠的。这一层主要有两个主要的功能:
- 对话控制(Dialog Control):会话层负责管理和控制两个系统之间的对话。它定义了对话的开始、结束和管理期间的规则。想象一下,如果你和朋友聊天,你们需要轮流发言,而不是同时说话,以保证对方能够听清楚。这是很有必要的,必须得轮流发言吧 ~
- 同步(Synchronization):会话层还确保数据的同步,防止因为某一方发送过快而导致对方无法跟上。它就像一个调控音乐会的指挥家,确保每个乐器都在正确的时间演奏。
总的来说,会话层就是负责确保通信的双方能够有序、有节奏地进行对话,就像你和朋友一起聊天一样。这样的层级设计使得通信更加可靠和有序。
- 表示层: 处理数据的格式,确保两个设备间的数据格式兼容。
表示层就好比你和朋友在聊天时使用的语言和表达方式。在计算机通信中,表示层负责将数据转换为适合传输的格式,以确保不同系统之间的数据能够正确解释和理解。这一层的功能主要包括:
- 数据格式转换(Data Format Conversion):表示层可以将数据从一种格式转换为另一种格式,以便接收方能够正确解释。就像你和朋友用不同语言交流时,可能需要翻译成对方能够理解的语言。
- 数据加密和解密(Data Encryption and Decryption):在通信中,为了保护数据的安全性,可能会对数据进行加密,表示层负责加密和解密这些数据。这就好比你和朋友之间有一种密码,只有你们两个能够解读对方发来的加密信息。
- 数据压缩和解压缩(Data Compression and Decompression):有时候,为了减少数据传输的时间和带宽占用,数据会被压缩。表示层负责对数据进行压缩和解压缩,就像你用手机发送图片时,手机会自动将图片压缩成更小的文件大小。
总的来说,表示层就是负责处理数据的格式、加密和压缩,以确保数据在传输过程中能够被正确解释和保护。这样的层级设计使得通信更加灵活和安全。
比如,你在编写程序时,可能会用到一些数据格式,比如 JSON、XML、或者 Protocol Buffers 等。这些格式规定了数据的结构和表示方法,而在表示层,数据就被转换成这样的格式,以便在网络上传输或者在不同系统之间进行交互。
举个例子,如果你的应用程序需要通过网络传输一些结构化的数据,你可能会选择使用 JSON 格式。在表示层,数据会被编码成 JSON 格式,以确保接收方能够正确解析和处理这些数据。同样,如果你从网络接收到 JSON 格式的数据,表示层会负责将其解码成程序能够理解的形式,以便你的应用程序能够对数据进行操作。
所以我们说,表示层对 数据的处理,对于程序员来说大部分还是透明的,我们可以自行选择 数据的格式,加密方式 等 ……
- 应用层: 提供用户接口,支持网络服务,如电子邮件和文件传输。
应用层就好比你在手机上使用的各种应用程序,它是计算机网络体系结构中最靠近用户的一层。在通俗的讲解中,可以这样理解:
应用层是用户与网络之间直接进行交互的地方,包含了我们日常使用的各种应用和服务。
举个例子,当你在浏览器中输入一个网址、发送一封电子邮件、使用即时通讯工具时,这些操作都发生在应用层。应用层提供了各种各样的网络服务,以满足用户的需求,比如:
- HTTP 协议(超文本传输协议):用于在 Web 浏览器和服务器之间传输超文本的协议。你在浏览器中访问网页时,实际上是在应用层使用 HTTP 协议进行通信。
- SMTP 协议(简单邮件传输协议):用于电子邮件的发送。当你发送一封电子邮件时,涉及到应用层使用 SMTP 协议进行邮件的传输。
- FTP 协议(文件传输协议):用于在网络上传输文件的协议。如果你下载或上传文件,可能就是通过 FTP 协议进行的。
在应用层,不仅包含了这些通用的协议,还包括了很多特定领域的应用,比如网络游戏、视频会议、文件共享等。总的来说,应用层提供了用户与网络交互的接口,是我们直接感知和使用的网络部分。
通俗讲解: OSI 参考模型是一个网络通信的规范,就像建筑物的蓝图一样,每个层级都有特定的职责,协同工作以实现可靠的通信。
数据链路层、网络层、传输层、会话层等网络协议的层次通常是由底层的网络设备和操作系统负责实现和管理的。这些层次提供了网络通信所需的基础设施,而应用程序通常不需要直接涉及这些层次的细节。这些层次的实现和细节通常是由底层的网络协议栈和硬件来处理的。
在应用层,程序员可以通过应用层协议(如HTTP、FTP、SMTP等)与其他应用程序进行通信。表示层主要负责数据的格式转换和编码,例如将数据转换为JSON格式。应用层和表示层是应用程序开发者通常直接参与和操作的层次。
因此,通常来说,应用程序的开发者主要关注应用层及以上的内容,通过高层次的API(例如Java中的Socket、HTTPURLConnection等)来进行网络编程,而不需要直接涉及到数据链路层、网络层和传输层的实现细节。这使得网络编程更加抽象和易用,开发者可以专注于应用逻辑而不必处理底层网络协议的复杂性。
1.2 TCP/IP 模型
- 链路层: 包括物理链路和数据链路层,负责数据帧的传输。(其实就是 物理层和数据链路层 的结合)
- 网络层: 处理数据包的寻址和路由选择。
- 传输层: 提供端到端的通信,包括 TCP 和 UDP。
- 应用层: 提供网络应用服务。
通俗讲解: TCP/IP 模型是实际互联网采用的模型,它把原来的七层模型合并为四层,更符合实际应用。就是对 七层模型进行了 简化!
1.3 IP 地址与端口
我们已经了解到,如果想要在网络上 找到一台计算机,想给人家 远程的进行数据交互,那你就得 知道对方的 IP 地址,而 端口这个东西,是 操作系统诞生之后,为了不同的 应用程序 作区分,设立的唯一标识。只要知道这个程序 与网络之间进行通讯用的是哪个端口号,那么我们 就可以 专门对这个应用程序 进行 网络信息的传输。这样彼此之间的 网络信息传输就都是独立的了 ~ 这种设计看似简单,实际上 在当时 也很难被想到,或者说经过好多的讨论,最后才 确定下来以 这种策略来 进行区分 程序!
- IPv4 和 IPv6: IP 地址是设备在网络上的标识,IPv4 使用 32 位地址,而 IPv6 使用 128 位地址。128 位的提出,是为了让 全世界的计算机,都能够分配到 IP 地址!而不出现 IPv4 的分配贫乏。就说现在的 外网IP 吧,知道云服务器为什么这么贵嘛?外网IP 为什么那么值钱嘛,就是因为 贫乏,我们整个亚洲地区好像 分到的份额惨不忍睹,自己可以去必应搜索一下。
- 端口号: 用于区分同一设备上不同的网络服务,范围从 0 到 65535。
通俗讲解: IP 地址就像是设备的电话号码,而端口号则是设备内部的分机号,确保数据能够准确传递到目标服务。
一般家庭网络服务商会为用户分配一个动态的IPv4地址,这个地址可能会在一段时间内变化。要在家里搭建服务器,你有以下几个选择:
- 使用动态DNS服务: 这样可以通过一个域名来访问你的家庭服务器,而不受IPv4地址变化的影响。
- 申请静态IP地址: 有些互联网服务提供商提供静态IP地址服务,但通常需要额外的费用。因为IP地址匮乏呀 ~
- 使用IPv6: 如果你的网络服务商支持IPv6,并且你的设备和网络设备都支持IPv6,那么你可以考虑使用IPv6来搭建服务器。你自己可以查一下,IPV6 基本上都是提示你 无权限,也就是 还没真的普及和启用。
第二章· Java 网络编程
Java 的网络编程是指使用 Java 编程语言进行网络应用程序的开发。在网络编程中,Java 提供了一系列的类和接口,使得开发者能够轻松地创建客户端和服务器端应用,实现网络通信和数据传输。
主要的网络编程相关的类和接口在 java.net
包中,其中一些关键的类包括:
- Socket 和 ServerSocket: 这两个类分别用于客户端和服务器端的套接字编程。
Socket
类用于创建客户端套接字,而ServerSocket
类用于创建服务器端套接字。
套接字(Socket)是网络编程中一种通信机制,它提供了在网络上运行的两个程序之间的通信机制。套接字允许在不同计算机之间的进程进行数据交换。在网络编程中,套接字通常用于建立客户端和服务器之间的通信连接。
套接字是一种抽象,它表示一个端点。每个套接字都与一个 IP 地址和端口相关联。套接字可以理解为通信的两个端点,一个用于发送数据,一个用于接收数据。
-
URL 和 URLConnection:
URL
类用于表示统一资源定位符,而URLConnection
类用于打开到指定 URL 引用资源的通信链接。 -
InetAddress: 用于表示 IP 地址。通过
InetAddress
类,可以获取主机名和 IP 地址之间的映射,以及执行与 IP 地址相关的其他操作。 -
DatagramPacket 和 DatagramSocket: 用于进行基于 UDP 协议的数据传输。
套接字又主要分为两种类型:
-
流套接字(Socket): 基于流的套接字提供了一种在网络上进行可靠的、双向的、面向连接的数据传输。它使用 TCP 协议 ,确保数据的可靠传输。在 Java 中,
Socket
类和ServerSocket
类就是用于处理流套接字的。 -
数据报套接字(DatagramSocket): 基于数据报的套接字提供了一种无连接的、不可靠的数据传输。它使用 UDP 协议,适用于一对多的通信。在 Java 中,
DatagramSocket
类用于处理 数据报 套接字。
- URLConnection: 用于打开到指定 URL 引用资源的通信链接,可以通过它读取和写入数据。
- SocketChannel 和 ServerSocketChannel:这两个类提供了基于通道的、非阻塞的套接字。通道是 NIO(New I/O)的一部分,提供了更灵活和高效的 I/O 操作。
网络编程的一般流程包括创建套接字、建立连接、进行数据传输、关闭连接等步骤。网络编程可以用于各种场景,包括构建客户端-服务器应用、实现网络通信的协议、创建分布式系统等。Java 提供了多种工具和框架,使得网络编程变得更加简单和灵活。
2.1 TCP 协议的消息传输
协议:就是约定,就好比我们现在说的是中文,那肯定要有一个人 会说中文,才能听懂我们说话,否则一个老外,肯定会认为我们 说的是 鸟语。。<这个约定 只是为了让我们 实现通讯和交流>
TCP/IP(用户传输协议/网络互连协议) 协议簇:它不是单指 一个协议,而是很多个协议的集合。再次强调一下 TCP/IP 是一组 协议,不是一个协议!
TCP:用于连接稳定的场景(效率较低),TCP 为什么安全?为什么效率低?我们将用通俗的语言,去介绍它的 三次握手和四次挥手的过程!
三次握手(Three-Way Handshake)
想象一下你和朋友打电话的场景:
-
第一步 - "你好"(SYN):
在网络中,这相当于客户端(你)向服务器(朋友)发送一个同步请求(SYN)。
可以理解为,你跟朋友打电话,说几句话做试探,然后等待对方回应。
-
第二步 - "我也好"(SYN + ACK):
服务器接收到请求后,回应一个同步和确认的消息(SYN + ACK),表示它也准备好了。
朋友那里同步了你说的那句话,听到了。然后回复你一个确认的消息,比如:”啊,听到了。“
-
第三步 - "开始通话"(ACK):
客户端再回应一个确认消息(ACK),表示通话正式开始。
待你也听到朋友说的 ”啊,听到了“之后,你也继续再回复他一声,作为确认"听到了是吧?那okok,咱们说正事!"
现在,你和朋友已经建立了连接,可以开始通话了。
四次挥手(Four-Way Handshake)
再想象一下结束通话的场景:
你和你的朋友,在使用一种特殊的通讯电话。需要双方都挂掉电话之后,才能真正的挂断。在这样的设备下,你与你的朋友,就会总是 开玩笑一般的不进行挂断,为了避免这种情况,我们可能需要询问第二次对方是否真的要挂断?这就是为什么挥手需要四次。
-
第一步 - "我说完了"(FIN => 朋友):
在网络中,这相当于客户端(你)向服务器(朋友)发送一个结束通话的请求(FIN)。
客户端发出连接关闭请求,表示不再发送数据,但仍愿意接收数据。
你:我聊累了,你还有啥想说的没有了?
-
第二步 - "我也说完了"(ack => 你):
服务器接收到结束请求后,回应一个确认消息(ACK),表示它也同意结束通话。
服务器收到客户端的 FIN 请求,若发送一个确认消息(ACK),就表示已经收到了客户端的关闭请求,但仍然允许数据传输。然后,服务器也发送一个 FIN,表示服务器自己也想要关闭发送数据的能力。
朋友:啊,我其实也没啥要说的了。
-
第三步 - "我确实说完了"(FIN => 你):
客户端再次发送一个结束请求(FIN),确认自己已经说完了。
朋友:看你这是真累了,那咱们今天就到这儿?
-
第四步 - "再见"(ACK => 朋友):
服务器再次回应一个确认消息(ACK),表示通话正式结束。
你:好好好,先这样,之后再说。
朋友<挂断了电话>
嘀嘀嘀 ~ 等待 2MSL 之后为什么客户端要等待2MSL?
主要原因是为了保证客户端发送那个的第一个ACK报文能到到服务器,因为这个ACK报文可能丢失,并且2MSL是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃,这样新的连接中不会出现旧连接的请求报文。哈哈 ~ 其实真实的原因是这样的!你<挂断了电话>
哎呀,先去休息休息,啪你也挂断了(再确认朋友那边确实挂断没有再发来消息之后,你也选择了挂断!)
这样,通话结束,连接也随之关闭。
步骤及其源代码
socket.getOutputStream();
:客户端获取到字节输出流,往里面写内容就属于是往 目标机器的那个端口所管辖的那片里面写内容。
socket.getInputStream();
客户端获取到字节输入流 …… 这个我认为都很简单吧 ~ I/O 流学的没问题,那就一点儿问题没有
观看我们之前的 TCP 那个代码,我们就可能已经发现了 服务器端拿到所谓的连接,其实是 通过 server.accept() 方法,拿到 客户端传输过来的数据,重新封装了一个 Socket 套接字对象。
这个 Socket 对象的输入流,就已经封装了 客户端发送过来的 数据。即,我们还是通过 Socket 对象来进行相关操作的,server 对象只是进行一个连接的作用,作为中间者。
客户端的 socket
- getInputStream() :拿到的是 服务器端 发送过来的 数据
- getOutputStream():向服务器端 发送数据
服务器端accept() 拿到的新 socket
- getInputStream() :拿到的是 客户端 发送过来的 数据
- getOutputStream():向客户端 发送数据
通俗来说,客户端活服务器端 它们的输入流 是对方发送过来的数据存储在 接收方的一块儿内存区域里面。而输出流 则是先把这些数据 放到内存中 待发送状态,然后通过网络协议的作用,逐步的发送给 接收方。
-
客户端 的简单编写
- 获取到 服务器的 IP地址
- 获取到 服务器端的 端口
- 建立 Socket 连接(IP地址,端口)就是创建了Socket 对象,这个对象一旦创建,就会去尝试 连接 Socket 服务。
- 获取到 客户端(Socket 连接对象) 的 输出流
- 在这个输出流里面 写 字节数据(数据块)
- 写完后,我们 关闭 输出流,再销毁 Socket 对象(断开连接)
-
服务器 的简单编写
- 直接利用 ServerSocket 创建 套接字服务(参数是需要你 自定义的端口)// IP 地址你没法自定义,因为 IP 地址 都是固定 分配给你的。。
- 根据 套接字服务的 .accept() 方法,我们可以 检测到 客户端 Socket 的连接请求,并直接连接上!该方法返回的 是一个 套接字连接 对象。
- 当我们 获取到 客户端的 套接字 连接对象后,我们就可以 获取到 它的 输入流。(也就是我们 刚才 输出的 数据,已经 被写到 套接字 输入流里面了。)
- 我们 还需要创建一个 管道流,来接收 我们的 获取到的数据,并且还需要 建立一个 byte[] 的数组。
- 那么管道流怎么获取数据呢?直接创建一个ByteArrayOutputStream() 的对象,然后 用 while 循环,不断的 让 socket 对象 的 .read(buffer) 方法 直接把数据读取到 buffer 数组里面。而我们 每次 循环 都需要判断 这个 方法 是否 返回 -1 因为 我们如果 再也搜索 不到 下一行数据的话,就会 返回 一个 -1 的字节长度。(代表着 没有下一行的 数据了!!)
- while 每次循环,都得 把数据 写到 baos 的 输出流里面。那要怎么去写呢?直接就写:管道符对象.write(buffer,0,len/字节长度/); 就写进去了!
- 最后我们 用 管道符对象.toString() 把 管道符 输出流的 数据 转换为 String 输出出来。
Socket 服务端
package www.muquanyu.lesson01;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
//服务端
public class TcpServerDemo01 {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = null;
Socket socket=null;
InputStream is =null;
ByteArrayOutputStream baos = null;
//1.我们得有个套接字(就是我必须给自己分配个 端口吧?)
serverSocket = new ServerSocket(9999);
while(true)
{
//2.等待客户端 连接通知,并连接客户端
socket = serverSocket.accept();
//3.读取客户端的消息
System.out.println(socket.getInetAddress());
System.out.println(socket.getPort());
// 大家思考一下,用 InputStreamReader 把这个字节流 转为 字符,是否可以正常?
is = socket.getInputStream();
// 字节数组输出流
baos = new ByteArrayOutputStream();
/*
使用 ByteArrayOutputStream 的方式可以一定程度上解决乱码问题,原因在于 ByteArrayOutputStream 只是简单地将字节存储在内存中,而不涉及字符集的转换。当你调用 baos.toString() 方法时,默认使用平台的默认字符集,这可能与你的数据的字符集一致,从而避免了一些乱码问题。
*/
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1)
{
System.out.println(len);
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
//关闭资源
/*if(baos != null)
{
baos.close();
}*/
/*if(is != null)
{
is.close();
}*/
/*if(socket != null)
{
socket.close();
}*/
/*if(serverSocket != null)
{
serverSocket.close();
}*/
}
/* 还要一种方式 但可能会出现一个问题,如果输入的是中文
// 那么 每个数据块大小不应该是 2个字节嘛
// 所以 就会 乱码。。
// byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer))!= -1){
String msg = new String(buffer, 0, len);
System.out.println(msg);
}*/
}
}
Socket 客户端
package www.muquanyu.lesson01;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
//客户端
public class TCPCilentDemo01 {
public static void main(String[] args) throws IOException {
Socket socket = null;
OutputStream os = null;
//1. 要知道服务器的地址
InetAddress serverIp = InetAddress.getByName("127.0.0.1");
//然后我们要知道 端口号
int port = 9999;
//然后 用地址 和 端口号 创建 一个 套接字连接
socket = new Socket(serverIp, port);
//发送消息 IO 流
os = socket.getOutputStream();
os.write("你好,我是客户端".getBytes());
//关闭资源
if(os != null)
{
os.close();
}
if(socket != null)
{
socket.close();
}
}
}
综上所述,就是 服务端的 ServerSocket 可以接收到 客户端的 Socket 对象,通过 这种方式 就可以进行 简易的 数据交互。
2.2 Socket 也可以进行文件的传输
我们其实 写 文件 上传,本质上也是利用 IO 流来实现的。
创建 文件 的 输入流,把待上传的文件 写到客户端Socket对象的输出流里面。 输出流里面。然后 通过 Socket 连接对象,服务器 端就可以 从 该对象身上 的 输入流里面 提取出 你刚才 写到 输出流里面的东西。
服务端
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TcpServerDemo02 {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9999);
Socket socket = serverSocket.accept();
InputStream is = socket.getInputStream();
FileOutputStream fos = new FileOutputStream("头像.jpg");
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
}
OutputStream os = socket.getOutputStream();
os.write("文件接收完毕!".getBytes());
socket.shutdownOutput();
//关闭资源
os.close();
fos.close();
is.close();
socket.close();
serverSocket.close();
}
}
- 客户端 要介绍通知服务器我已传输完毕的一个方法(关闭socket输出流!只要你一关闭,服务器端 就会认为 你传输完毕!!)
//通知服务器,我已传输完毕 socket.shutdownOutput();
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
public class TcpClientDemo02 {
public static void main(String[] args) throws IOException {
InetAddress IP = InetAddress.getByName("127.0.0.1");
int port = 9999;
Socket socket = new Socket(IP, port);
OutputStream gops = socket.getOutputStream();
FileInputStream fis = new FileInputStream("src/头像.jpg");
byte[] buffer = new byte[1024];
int len;
int i = 1;
while((len = fis.read(buffer)) != -1){
gops.write(buffer,0,len);
System.out.println("第"+(i++)+"行数据,写入完毕!");
}
// 有的时候需要关闭掉流,才能够 正常的顺利的 关闭掉 socket 通讯!
socket.shutdownOutput();
// gops.flush(); 其实也是可以保证传递完的
InputStream inputStream = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer2 = new byte[1024];
int len2 = 0;
while((len2 = inputStream.read(buffer2)) != -1){
baos.write(buffer2, 0, len2);
}
System.out.println(baos.toString());
socket.shutdownInput();
//通知服务器,我已经传输完毕
baos.close();
inputStream.close();
fis.close();
gops.close();
socket.close();
}
}
2.3 Socket 服务端 与 浏览器客户端进行交互
学习 WEB 之前,其实一定要搞清楚 网络编程,这是因为 WEB 的一些接口、类、方法 其实都涉及到 网络编程的相关知识。
我们的浏览器 就是一个客户端,而我们可以用 Java 写一个 服务端程序。
如果浏览器是一个客户端,那么它如何 去连接/访问 我们写好的 服务端程序呢?
答:在地址栏上 输入 IP/端口
直接回车即可
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class SimpleHttpServer {
public static void main(String[] args) {
int port = 8080;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器正在监听端口 " + port);
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress());
handleClientRequest(clientSocket);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handleClientRequest(Socket clientSocket) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
OutputStream outputStream = clientSocket.getOutputStream()) {
// 读取客户端的请求
String request = reader.readLine();
System.out.println("Received request: " + request);
// 向客户端发送简单的HTTP响应
String response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain;charset=utf-8\r\n\r\nHello, this is a simple HTTP server!";
outputStream.write(response.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
第三章·UDP 利用云服务器实现聊天
UDP(User Datagram Protocol)是一个无连接的、简单的传输层协议。与TCP不同,UDP不进行握手,没有连接的建立和断开过程。UDP主要提供了一种尽力而为的数据传输服务,但不保证可靠性和有序性。
- 发送端准备数据: 发送端应用程序准备需要发送的数据。
- 数据封装: 将应用程序数据封装为UDP数据包。UDP数据包包括目标端口号、源端口号、长度和校验和等信息。
- 发送数据包: 将封装好的UDP数据包通过网络发送给目标主机(IP)的UDP端口。
- 接收数据包:接收端的UDP协议栈接收到UDP数据包
- 数据解封: 接收端的应用程序从UDP数据包中解封出 应用层数据。
整个过程没有连接的建立和断开,也没有像TCP那样的确认机制。UDP的优点是简单、高效,适用于一些要求实时性较高,对数据准确性要求相对较低的场景,如音视频传输、实时游戏等。但UDP不保证数据的可靠性,因此在可靠性要求高的场景下,更常使用TCP。
可以跟大家说下,我们的语音、视频聊天之类的其实大多数用的都是 UDP,因为UDP 效率比较高嘛,低延迟和具有实时性。响应比较快嘛 ~ 而且我们知道对于人类来说,遇到丢包,也就是 视频卡住,或者声音 突然赛博朋克的情况,是会自我调节的,就是我们会知道 是不是卡了??是不是丢包了?? 麻烦你再说一遍,或者刚才 我没看到那个画面。
而 TCP 更多是应用于 文本消息,保证其可靠性和稳定性。而不要求那么高的实时性。
以前的话呢,学过 网络编程 那里,写了一系列的 笔记。但是 那时候 没租 云服务器,所以根本 就不算是 真正的 学习 网络编程。
这次的话,我把写的 服务器 程序 搭在 我租的阿里云服务器上。然后 我们 进行了 一系列的 尝试。其中 我认为 我写的 最好的,就是 UDP 利用 云服务器 弄一个 简易 聊天室。当然 这个 聊天室 只 限于 一对一。也就是 我们 在 本机 上 打开 客户端,然后 服务器上 打开服务端,可以 进行 一对一的 信息 传输。
- 服务器端
import java.io.BufferedReader;
import java.io.DataOutput;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.*;
public class 服务端 {
public static int daPort = 0; // NAT 动态分配 的 端口
public static InetAddress publicIP = null; // NAT 转换而成 的 公网 IP
public static DatagramSocket socket = null;
public static void main(String[] args) throws IOException {
/* 由 端口 10000 创建 一个 套接字UDP 服务 */
socket = new DatagramSocket(10000);
/* 创建 一个 SendMsg 线程 并设为 该线程为 守护线程!开启 该 线程 进行 发送消息的 监视 */
SendMsg sendMsg = new SendMsg();
sendMsg.setDaemon(true);
sendMsg.start();
/* 以下 是 接收 消息的监视,由 主线程 管理 */
while(true){
byte[] container = new byte[1024];
DatagramPacket packet = new DatagramPacket(container,0,container.length);
// 阻塞 接收 UDP 发送来的 数据包
socket.receive(packet);
// 把数据包的 数据 存储到 相应类型 的 容器中
byte[] data = packet.getData();
// 进行 String 的 转换,这样方便 输出
String dataString = new String(data,0,getSize(data));
// 如果 接收到的 是 心跳包,我们 就 不去 输出它 而是 更新 我们的 动态分配端口 和 公网IP
if(dataString.equals("HeartBeat")){
//System.out.println("接收到 HeartBeat");
daPort = packet.getPort();
publicIP = packet.getAddress();
}else if(dataString.equals("bye")){
// 当 接收到 bye 字符串的时候,我们就应该 退出了!要去 关闭 套接字服务了。这算是一个 字符串口令。
break;
}else{
// 发送 正常接收到的 数据
System.out.println(dataString);
}
}
// 其实 有没有 bye 口令 都无所谓,我们只要 在 客户端 做一个 断开连接 就行的。
socket.close();
}
// 读取 byte[] 数组 有效的 数据 位数,以便于 更好 的 转换 为 String 类型
public static int getSize(byte[] data){
int i = 0;
for(byte x: data){
if(x != (byte)0){
i++;
}
}
return i;
}
// SendMsg 线程 为了省事,而且本来 线程 开的 也少,就 采用了 继承 Thread 的方式
public static class SendMsg extends Thread{
@Override
public void run() {
super.run();
while(true){
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String data = null;
try {
data = reader.readLine();
} catch (IOException e) {
e.printStackTrace();
}
byte[] datas = data.getBytes();
DatagramPacket packet = new DatagramPacket(datas,0,datas.length,publicIP, daPort);
try {
socket.send(packet);
System.out.println("发送数据包完毕!");
//socket.send(packet);
} catch (IOException e) {
e.printStackTrace();
}
if(data.equals("bye")){
System.out.println("已关闭socket服务!");
break;
}
}
socket.close();
}
}
}
这里 有几个 细节 部分 需要注意。我们 无时无刻 都在 接收 数据包,但是 却 判断了 一个 字符串 HeartBeat
,这是什么呢? 这个 其实 就是 我们 发送 的 心跳包。即 每隔一段时间 就要发送 一个 数据包过来,俗称 心跳包。
① 那么为什么客户端 要发送 心跳包呢 ?
答:因为 UDP 的 连接 保持 活性的 时间 是 很短的。所以 我们 要 每隔一段时间 发送 一个数据包,来保持 连接的活性。
② 为什么 要 把 发送过来的数据包 IP 和 Port 获取下来呢 ?
答:因为 我们的本机 属于内网,它 要 通过 NAT 进行 端口的动态分配,还有 转换为 外网 IP,这样的话 我们 暂时 认为在基于 这个外网IP 和 动态分配端口的情况下,双方的连接通讯 是 安全的。可以进行 有效的 数据发送和接收。
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
public class 客户端 {
// 提高 作用域
public static JTextArea textArea = null;
public static DatagramSocket socket = null;
public static void main(String[] args) throws IOException {
// 绘制 简单 JFrame GUI
JFrame frame = new JFrame("UDP 客户端");
frame.setBounds(500,500,500,500);
Container container = frame.getContentPane();
container.setLayout(new GridLayout(3,1));
JTextArea textAreaA = new JTextArea("请输入文字内容",10,40);
JPanel jPanel1 = new JPanel();
JPanel jPanel2 = new JPanel();
JPanel jPanel3 = new JPanel();
jPanel3.setLayout(new GridLayout(1,2));
JTextArea textAreaB = new JTextArea("服务器端发来的数据:\n",10,40);
textAreaA.setLineWrap(true);
textAreaB.setLineWrap(true);
JScrollPane jScrollPaneA = new JScrollPane(textAreaA);
JScrollPane jScrollPaneB = new JScrollPane(textAreaB);
textAreaB.setEditable(false);
jPanel1.add(jScrollPaneA);
jPanel2.add(jScrollPaneB);
container.add(jPanel1);
container.add(jPanel2);
JButton bthSend = new JButton("点击发送消息");
JButton bthClose = new JButton("安全关闭连接");
jPanel3.add(bthSend);jPanel3.add(bthClose);
container.add(jPanel3);
// 指定一个 端口 开启 套接字 UDP 服务
socket = new DatagramSocket(10000);
// 获取到 textAreaB
textArea = textAreaB;
// 创建一个 receiveMsg 线程,用来 监视 接收 到的数据
ReceiveMsg receiveMsg = new ReceiveMsg();
// 设为 守护线程
receiveMsg.setDaemon(true);
receiveMsg.start();
// 创建一个 heartBeat 线程,每隔 三十秒 发送一个 心跳包
HeartBeat heartBeat = new HeartBeat();
// 设为 守护线程
heartBeat.setDaemon(true);
heartBeat.start();
// 初次连接,我们 一定要 发送一个 数据包 进行 连通验证,当我们 在 服务端 接收到这条 信息的时候 才能证明 我们连通了
byte[] datas = "连接成功".getBytes();
DatagramPacket packet = new DatagramPacket(datas, 0, datas.length, new InetSocketAddress("云服务器IP", 10000));
socket.send(packet);
// 发送按钮的 触发 监听
bthSend.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String msg = textAreaA.getText();
byte[] datas = msg.getBytes();
DatagramPacket packet = new DatagramPacket(datas,0,datas.length,
new InetSocketAddress("云服务器IP", 10000));
try {
socket.send(packet);
} catch (IOException ex) {
ex.printStackTrace();
}
System.out.println(packet.getPort());
textAreaA.setText("");
}
});
// 关闭 服务 和 线程 按钮的 触发 监听
bthClose.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if(!socket.isClosed()){
socket.close();
}
receiveMsg.stop();
frame.setTitle("连接已全部断开!线程已关闭!");
}
});
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
public static class ReceiveMsg extends Thread{
@Override
public void run() {
super.run();
while(true){
byte[] containner = new byte[1024];
DatagramPacket packet = new DatagramPacket(containner,0,containner.length);
try {
socket.receive(packet);
//System.out.println(packet.getPort());
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = packet.getData();
String dataString = new String(data,0,getSize(data));
//System.out.println(dataString);
textArea.append(dataString+"\n");
}
}
// 读取 byte[] 数组 有效的 数据 位数,以便于 更好 的 转换 为 String 类型
public static int getSize(byte[] data) {
int i = 0;
for(byte x:data)
{
if(x != (byte)0)
{
i++;
}
}
return i;
}
}
public static class HeartBeat extends Thread{
@Override
public void run() {
super.run();
String heartBeat = "HeartBeat";
while(true){
DatagramPacket packet = new DatagramPacket(heartBeat.getBytes(), 0,heartBeat.length(),new InetSocketAddress("云服务器IP", 10000));
try {
socket.send(packet);
} catch (IOException e) {
e.printStackTrace();
}
try {
// 建议 每 三十秒 发送 一次 心跳包,因为 UDP 协议 生命周期 很短。必须 隔一段时间 发送心跳包 保持活性
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
然后 我们 整体的 思路 就是,把 云服务器的 这个 10000
端口 开放,在 客户端 和 服务器那里 各开 一个 线程,客户端的子线程 用来 接收 服务器端 发送的 数据包,服务器端的 子线程 用来 发送 给客户端 数据包。
但是 我们客户端 还得再 建一个 线程发送 心跳包。保持 连接的活性。
这样,我们 只在 云服务器上 开放了 一个 10000
端口,就实现了 云服务器 和 本机 的 数据包通讯 。