通信协议
根据ip
可以找到主机,根据端口
可以找到要给哪个进程发信息,接下来就是通过遵循相同的协议
来发送信息。
本节主要讲解TCP、UDP两种协议
TCP、UDP的区别:
可以把 TCP 和 UDP 这两种协议想象成两种不同的送快递方式,下面为你详细对比它们的区别:
送快递前是否先联系
- TCP(像靠谱的快递员):送快递前会和收件人先联系确认。比如快递员打电话问收件人 “在不在家呀,我准备送快递过来啦”,收件人回复 “在呢,你送来吧”,快递员回复“好的,我来了”。在网络里,这就是 TCP 的 “三次握手”,在传数据前先建立可靠的连接,确保对方能接收数据。就像你用浏览器打开网页,浏览器和网站服务器之间就会先通过 TCP 协议建立连接,确认没问题了才开始传网页内容。
- UDP(像风风火火的快递员):不提前联系收件人,直接把快递放快递柜就走。它不关心收件人在不在家、能不能及时拿到快递。在网络中,UDP 协议发送数据前不会建立连接,直接就把数据发出去,简单又快速。比如在线玩游戏或者看视频直播,用 UDP 协议传输数据,能保证游戏画面和视频的流畅性,就算偶尔丢点数据,也不太影响整体体验。
送快递过程是否确保完整
- TCP(认真负责到底):送快递过程中会确保每一个包裹都准确无误地送到收件人手里。如果发现有包裹丢了或者损坏了,会重新送一次。在网络传输里,TCP 协议会对数据进行编号,接收方收到数据后会反馈给发送方,如果发送方发现有数据没收到反馈,就会重新发送,保证数据完整、顺序正确。就像你下载一个大文件,用 TCP 协议下载,文件不会缺胳膊少腿,能完整地下载到你的电脑里。
- UDP(主打一个速度):不太在意包裹是否完整送到。它只管尽快把包裹送出去,至于包裹有没有丢失、有没有损坏,它不会去管。在网络中,UDP 协议不会对数据进行严格的检查和重传。还是以在线游戏为例,可能偶尔会出现画面卡顿或者瞬移的情况,就是因为 UDP 协议传输时可能丢了一些数据,但为了保证速度,就不重新传了。
送快递的效率
- TCP(稳扎稳打但慢些):因为要建立连接、确认数据完整,所以整个过程比较繁琐,花费的时间相对多一些。但它能保证数据可靠传输,适合对数据准确性要求高的场景,比如文件传输、发送邮件等。
- UDP(快如闪电但不太稳):没有那些繁琐的步骤,直接发送数据,所以效率很高,速度很快。不过因为不太保证数据完整,所以适合对实时性要求高、对数据准确性要求相对低的场景,像语音通话、视频直播等。
TCP实现聊天
示例代码
客户端
- 连接服务器Socket
- 发送消息
package protocol;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
// Demo01的客户端
public class TcpClientDemo01 {
public static void main(String[] args) {
Socket socket = null;
OutputStream os = null;
try {
// 1.要知道服务器的地址和端口号
InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 9999;
// 2.根据地址和端口创建socket连接
socket = new Socket(address,port);
// 3.使用io流给服务端发送信息
os = socket.getOutputStream();
os.write("喜欢的话给我的文章点个收藏".getBytes());
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 关闭遵循先开后关
// 关闭流对象
if(os != null){
try {
os.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 关闭套接字
if(socket != null){
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
代码解释
- 获取服务器地址:
InetAddress address = InetAddress.getByName("127.0.0.1");
:通过InetAddress.getByName
方法获取 IP 地址为127.0.0.1
(本地回环地址)的InetAddress
对象。int port = 9999;
:指定服务器的端口号为9999
。
- 创建套接字连接:
socket = new Socket(address,port);
:使用获取到的 IP 地址和端口号创建一个Socket
对象,从而建立与服务器的 TCP 连接。
- 发送数据:
os = socket.getOutputStream();
:通过socket.getOutputStream()
方法获取与该Socket
关联的输出流。os.write("喜欢的话给我的文章点个收藏".getBytes());
:将字符串 “喜欢的话给我的文章点个收藏” 转换为字节数组,并通过输出流发送给服务器。
示例代码
服务端
- 建立服务的端口 ServerSocket
- 等待用户连接 accept
- 接收客户端的消息
package protocol;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
// Demo01的服务端
public class TcpServerDemo01 {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
ByteArrayOutputStream baos = null;
try {
// 1.建立服务端口
serverSocket = new ServerSocket(9999);
// 2.客户端创建连接,服务端接收连接
socket = serverSocket.accept(); // 这里的socket其实就是客户端那里的socket对象
// 3.读取客户端的消息,使用io流和管道流
baos = new ByteArrayOutputStream();
// 缓冲区
byte[] buf = new byte[1024];
// 传过来的信息的字节长度
int len = 0;
while ((len = socket.getInputStream().read(buf)) != -1) {
// 表示流里面还有内容
// 写到缓冲区中
baos.write(buf, 0, len);
}
// 输出内容
System.out.println(baos.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
if(baos!=null) {
try {
baos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(socket!=null) {
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(serverSocket!=null) {
try {
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
代码解释
-
accept()
方法是一个阻塞方法,会一直等待,直到有客户端连接到该服务器。当有客户端连接时,该方法会返回一个Socket
对象,用于与客户端进行通信。这个Socket
对象与客户端的Socket
对象共同构成了客户端与服务器之间的连接通道。 -
byte[] buf = new byte[1024];
:创建一个长度为 1024 的字节数组buf
,作为临时缓冲区,用于存储从客户端输入流中读取的数据。 -
while ((len = socket.getInputStream().read(buf)) != -1)
:通过socket.getInputStream().read(buf)
方法从客户端的输入流中读取数据到buf
数组中,并返回实际读取的字节数len
。当len
为 -1 时,表示已经读取到流的末尾,循环结束。 -
baos.write(buf, 0, len);
:将buf
数组中从索引 0 开始、长度为len
的字节数据写入到ByteArrayOutputStream
对象baos
中。 -
调用
baos.toString()
方法将ByteArrayOutputStream
中存储的字节数据转换为字符串,并通过System.out.println()
方法输出到控制台。
帮助理解
1、 为什么客户端用OutputStream,服务端用InputStream,缓冲区用ByteArrayOutputStream?
+----------------+ +----------------+ +----------------+
| 客户端 | --(os流)--> | 网络通道 | --(is流)--> | 服务端 |
| | | | | |
| 数据产生地 | | 数据传输路径 | | 数据接收地 |
| | | | | |
| OutputStream | | | | InputStream |
+----------------+ +----------------+ +----------------+
|
v
+------------------------+
| 缓冲区 |
| |
| 临时存储区 |
| |
| ByteArrayOutputStream |
+------------------------+
2、开启的流对象
,Socket
,SockeServer
都要关闭,关闭顺序遵循先开后关,即谁最先开启的服务谁就最后关闭。
3、先开启服务端,再开启客户端。
4、ByteArrayOutputStream
原理概述:
ByteArrayOutputStream
是 Java I/O 库中的一个类,它继承自 OutputStream
。它的主要作用是将数据写入一个字节数组中,这个字节数组会随着写入数据的增加而动态扩展。你可以把它想象成一个内存中的缓冲区,程序产生的字节数据可以源源不断地写入其中,直到数据处理完毕。
ByteArrayOutputStream
内部维护了一个字节数组,用于存储写入的数据。当你调用 write
方法写入数据时,数据会被追加到这个字节数组的末尾。如果写入的数据超过了当前字节数组的容量,ByteArrayOutputStream
会自动创建一个更大的字节数组,并将原来的数据复制到新数组中,然后继续写入新的数据。
5、主要要理解客户端和服务端是怎么发送信息的(必看%%%%%%)
- 服务端找个端口
- 客户端根据本机ip和端口,向服务端请求连接
- 服务端用serverSocket.accept()建立连接
- 客户端os流向服务端发送消息
- 服务端用is流接收并写在缓冲区
并使用
ByteArrayOutputStream
对象baos
作为缓冲区接收数据,好处在于其能动态扩展容量以适应不定长数据、方便整合分散数据包、避免频繁内存分配与复制来提升性能、支持灵活的数据处理方式,且资源管理简单。
- 最后输出缓冲区的数据即可
TCP实现文件上传
接收文件就用文件的管道流,接收字节数组就用字节数组的管道流(ByteArrayOutputStream
),缓冲区存在于is和os之间,就相当于只要有is和os传输数据就要有一个缓冲区来充当中转站。
示例代码(客户端)
package protocol;
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 Exception {
Socket socket=new Socket(InetAddress.getByName("127.0.0.1"),9999);
FileInputStream fis=new FileInputStream("god.jpg");
OutputStream os = socket.getOutputStream();
byte[] bytes = new byte[1024];
int len;
while ((len = fis.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
// 传完了通知服务端我已经结束了,但是此时并没有关闭socket
socket.shutdownOutput(); // 然后服务端就去接收了
// 服务端接收后发通知过来告诉客户端可以结束了,服务端传的是字节流
InputStream is=socket.getInputStream();
// 打印到终端,或者作为中间数据才用,若是只用存起来,直接用fos写到文件即可
ByteArrayOutputStream bos=new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len1 = 0;
while ((len1 = is.read(buf)) != -1) {
bos.write(buf, 0, len1);
}
System.out.println(bos.toString());
bos.close();
is.close();
os.close();
fis.close();
socket.close();
}
}
代码步骤解释
- 创建连接:通过
Socket
类连接到服务端的指定地址和端口。 - 读取文件:使用
FileInputStream
打开本地文件god.jpg
。 - 发送数据:利用
Socket
的OutputStream
将文件内容逐块发送给服务端。 - 通知结束:调用
socket.shutdownOutput()
告知服务端数据发送完毕。 - 接收响应:获取
Socket
的InputStream
接收服务端的响应,并使用ByteArrayOutputStream
存储。 - 打印响应:将接收到的响应数据转换为字符串并打印。
- 关闭资源:依次关闭所有使用的流和
Socket
连接。
通过以上的客户端和服务端代码,实现了基于 TCP 协议的文件传输和消息交互功能。
示例代码(服务端)
package protocol;
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 Exception {
ServerSocket serverSocket=new ServerSocket(9999);
Socket socket=serverSocket.accept();
InputStream is=socket.getInputStream();
FileOutputStream fos = new FileOutputStream("receive.jpg");
byte[] b=new byte[1024];
int len;
while((len=is.read(b))!=-1) {
fos.write(b, 0, len);
}
// 接收完了通知用户端可以关闭了
OutputStream os=socket.getOutputStream();
// 将String以字节流传过去
os.write("我已经接收到了,你可以关闭了".getBytes());
os.close();
fos.close();
is.close();
socket.close();
serverSocket.close();
}
}
代码步骤解释
- 启动监听:创建
ServerSocket
并绑定到端口 9999,开始监听客户端连接。 - 接受连接:使用
accept()
方法等待客户端连接,连接成功后返回一个Socket
对象。 - 获取输入流:通过
Socket
获取客户端的输入流,准备接收数据。 - 创建输出文件:使用
FileOutputStream
创建本地文件receive.jpg
,用于存储接收到的数据。 - 接收数据:从输入流读取数据,并将其写入本地文件。
- 发送通知:获取
Socket
的OutputStream
,将通知消息以字节流形式发送给客户端。 - 关闭资源:依次关闭所有使用的流和
Socket
连接,最后关闭ServerSocket
。
讲解出现的缓冲区
客户端出现了两次
第一次是将文件传到服务端的文件读取缓存区:是将读文件流fis---->套接字的os流
- 作用:
byte[] bytes
数组作为一个缓冲区,用于从本地文件god.jpg
中读取数据。FileInputStream
的read
方法会尝试从文件中读取最多 1024 字节的数据到bytes
数组中,并返回实际读取的字节数len
。通过使用缓冲区,避免了每次只读取一个字节的低效操作,减少了与磁盘的交互次数,提高了文件读取的效率。 - 原理:磁盘 I/O 操作相对较慢,每次读取一个字节会频繁地进行磁盘寻道和数据传输,而使用缓冲区可以一次性读取多个字节,将多次小的磁盘 I/O 操作合并为一次较大的操作,从而提高性能。
第二次是接收服务端发来的字节流数据的数据接收缓冲区(byte[] buf
)和 ByteArrayOutputStream
:是将套接字的is流转换为接收字节流的bos
byte[] buf
的作用:作为从Socket
输入流中读取数据的缓冲区。InputStream
的read
方法会尝试从网络连接中读取最多 1024 字节的数据到buf
数组中,并返回实际读取的字节数len1
。这样可以减少与网络的交互次数,提高数据接收的效率。ByteArrayOutputStream bos
的作用:ByteArrayOutputStream
是一个内存缓冲区,用于将从网络接收到的数据临时存储起来。每次从buf
中读取到有效数据后,将其写入bos
中。ByteArrayOutputStream
会自动管理内部的字节数组,当数据量超过当前数组容量时会自动扩容,方便将接收到的分散数据整合为一个完整的数据块。
服务端出现了一次
接收文件的文件写入缓冲区:将套接字的is流---->fos
- 作用:
byte[] b
数组作为从Socket
输入流中接收数据的缓冲区,同时也是向本地文件receive.jpg
写入数据的中间存储区。InputStream
的read
方法将从网络连接中读取的数据存储到b
数组中,然后FileOutputStream
的write
方法将b
数组中实际读取的有效数据写入到文件中。通过使用缓冲区,减少了与网络和磁盘的交互次数,提高了数据接收和文件写入的效率。