在使用 Java 进行网络通信之前,我们需要先了解一些网络的概念。
网络架构
常见的网络架构分为 C/S 架构和 B/S 架构:
- C/S 架构:主从式架构 (
Client–server model
), 也称客户端-服务器(Client/Server
)架构、C/S架构,是一种网络架构,它把客户端 (Client
) (通常是一个采用图形用户界面的程序)与服务器 (Server
) 区分开来。每一个客户端软件的实例都可以向一个服务器或应用程序服务器发出请求。有很多不同类型的服务器,例如文件服务器、游戏服务器等。 - B/S架构:浏览器-服务器(
Browser/Server
)架构,简称B/S架构,与C/S架构不同,其客户端不需要安装专门的软件,只需要浏览器即可,浏览器通过Web服务器与数据库进行交互,可以方便的在不同平台下工作;服务器端可采用高性能计算机,并安装Oracle、Sybase、Informix等大型数据库。B/S架构简化了客户端的工作,它是随着Internet技术兴起而产生的,对C/S技术的改进,但该架构下服务器端的工作较重,对服务器的性能要求更高。
两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程就是在一定的协议下,实现两台计算机的通信的程序。
网络传输
什么是网络传输协议?
我们知道:通过计算机网络可以使多台计算机实现连接,那么位于同一个网络中的计算机在进行连接和通信时肯定需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。
而网络中的协议就是代表着规则。
在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守这些协议才能完成数据的交换。
网络模型
TCP/IP模型:TCP/IP提供点对点的链接机制,将数据应该如何封装、定址、传输、路由以及在目的地如何接收,都加以标准化。它将软件通信过程抽象化为四个抽象层,采取协议堆栈的方式,分别实现出不同通信协议。协议族下的各种协议,依其功能不同,被分别归属到这四个层次结构之中[7][8],常被视为是简化的七层OSI模型。
下面是TCP/IP模型图示:
上图中,TCP/IP协议中的四层从下到上依次是链路层、网络层、传输层和应用层,每层都分别负责不同的通信功能,每层都对应有不同的协议:
- 链路层:链路层实际上并不是因特网协议组中的一部分,但是它是数据包从一个设备的网络层传输到另外一个设备的网络层的方法。这个过程能够在网卡的软件驱动程序中控制,也可以在韧体或者专用芯片中控制。用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。
- 网络层:网络层解决在一个单一网络上传输数据包的问题,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。一些IP承载的协议,如ICMP(用来发送关于IP发送的诊断信息)和IGMP(用来管理多播数据),还有ARP(地址解析协议)通过网络地址来定位MAC地址,RARP(逆地址解析协议)用于将MAC地址转换为IP地址
- 传输层:能够解决诸如端到端可靠性(“数据是否已经到达目的地?”)和保证数据按照正确的顺序到达这样的问题。在进行网络通信时,根据传输的是否可靠,分为TCP协议和UDP协议。
- 应用层:该层包括所有和应用程序协同工作,利用基础网络交换应用程序专用的数据的协议。 应用层是大多数普通与网络相关的程序为了通过网络与其他程序通信所使用的层。这个层的处理过程是应用特有的;数据从网络相关的程序以这种应用内部使用的格式进行传送,然后被编码成标准协议的格式。一些特定的程序被认为运行在这个层上。它们提供服务直接支持用户应用。这些程序和它们对应的协议包括HTTP(万维网服务)、FTP(文件传输)、SMTP(电子邮件)、SSH(安全远程登陆)、DNS(名称<-> IP地址寻找)以及许多其他协议。 一旦从应用程序来的数据被编码成一个标准的应用层协议,它将被传送到IP栈的下一层。->
IP协议
网际协议(Internet Protocol,缩写:IP;也称互联网协议)是用于分组交换数据网络的一种协议。
IP是在TCP/IP协议族中网络层的主要协议,其任务仅仅是根据源主机和目的主机的地址来传送数据。为此目的,IP定义了寻址方法和数据报的封装结构。
作用:寻址和路由
IP协议最为复杂的方面可能就是寻址和路由了。
寻址就是如何将IP地址分配给各个终端节点,以及如何划分和组合子网。
路由:就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。
所有网络端点都需要路由,尤其是网际之间的路由器。路由器通常用内部网关协议(Interior Gateway Protocols,IGPs)和外部网关协议(External Gateway Protocols,EGPs)决定怎样发送IP数据包。
IP地址
互联网协议地址(Internet Protocol Address,又译为网际协议地址),缩写为IP地址(英语:IP Address),是分配给网络上使用网际协议(Internet Protocol, IP)的设备的数字标签。常见的IP地址分为IPv4与IPv6两大类,但是也有其他不常用的小分类。
TCP协议
TCP协议是面向连接的通信协议,即在传输数据前先在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。
三次握手
在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”:
- 第一次握手,客户端向服务器端发出连接请求(客户端给服务器发送一个
SYN
报文),等待服务器确认; - 第二次握手,服务器端向客户端回送一个响应(服务器收到
SYN
报文之后,会应答一个SYN+ACK
报文),通知客户端收到了连接请求; - 第三次握手,(客户端收到
SYN+ACK
报文之后,会回应一个ACK
报文),客户端再次向服务器端发送确认信息,确认连接。
整个交互过程如下图所示:
服务器收到 ACK 报文之后,完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。
作用
- 确认双方的接受能力、发送能力是否正常。
- 指定自己的初始化序列号,为后面的可靠传送做准备。
- 如果是 HTTPS 协议的话,三次握手这个过程,还会进行数字证书的验证以及加密密钥的生成
优点
由于TCP协议面向连接的特性,可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。
UDP协议
UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
优点
由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输,例如视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
缺点
但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。
TCP VS UDP
UDP | TCP |
---|---|
无连接 | 面向连接 |
一对一、一对多、多对一、多对多 | 一对一 |
对应用层交付的报文直接打包 | 面向字节流 |
不可靠,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
首部开销小,仅 8 字节 | 首部最小 20 字节,最大 60 字节 |
套接字:Socket
维基百科的解释:
在计算器科学中,网上套接字(
Network socket
),又译网络套接字、网络接口、网上插槽,是计算机网上中进程间数据流的端点。使用以网际协议(Internet Protocol)为通信基础的网上套接字,称为网际套接字(Internet socket)。因为网际协议的流行,现代绝大多数的网上套接字,都是属于网际套接字。
socket是一种操作系统提供的进程间通信机制。[1]
在操作系统中,通常会为应用程序提供一组应用程序接口(API),称为套接字接口(socket API)。应用程序可以通过套接字接口,来使用网上套接字,以进行数据交换。最早的套接字接口来自于4.2 BSD,因此现代常见的套接字接口大多源自Berkeley套接字(Berkeley sockets)标准。在套接字接口中,以IP地址及通信端口组成套接字地址(socket address)。远程的套接字地址,以及本地的套接字地址完成连线后,再加上使用的协议(protocol),这个五元组(five-element tuple),作为套接字对(socket pairs),之后就可以彼此交换数据。例如,在同一台计算机上,TCP协议与UDP协议可以同时使用相同的port而互不干扰。 操作系统根据套接字地址,可以决定应该将数据送达特定的进程或线程。这就像是电话系统中,以电话号码加上分机号码,来决定通话对象一般。
总结起来就是:
套接字 = IP地址 + 通信端口(port
) + 传输协议(TCP/UDP
)
IP地址对应一台主机,通信端口(port
) 对应这台主机上的各个应用程序,不同的应用程序占用的端口不同,而传输协议又分为TCP和UDP。
Java中的网络通信
Java中的java.net
包提供了对TCP
和UDP
协议的支持。
TCP通信
TCP通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。
C/S通信的步骤:
①服务端程序,需要事先启动,之后等待客户端的连接。
②客户端主动连接服务器端,连接成功才能通信。
注意哦:
服务端不可以主动连接客户端
在Java中,提供了两个类用于实现TCP通信程序:
①java.net.Socket
类:代表客户端。创建Socket
对象,其向服务端发出连接请求,当服务端响应请求后,两者建立连接并开始通信。
②java.net.ServerSocket
类:代表服务端。创建ServerSocket
对象,相当于开启一个服务,并等待客户端的连接。
Socket类
Socket
类:实现了客户端套接字
构造方法
Socket
类的构造方法如下:
public Socket(String host, int port)
:创建套接字对象并将其连接到指定主机上的指定端口号。如果指定要连接的主机号(host
)为null
,则相当于指定地址为回送地址。
示例代码:1
Socket client = new Socket("127.0.0.1", 6666);
成员方法
Socket
类的成员方法如下:
public InputStream getInputStream()
: 返回此套接字的输入流。- 如果此
Socker
具有相关联的通道,则生成的InputStream
的所有操作也关联该通道。 - 关闭生成的
InputStream
也将关闭相关的Socket
。
- 如果此
public OutputStream getOutputStream()
: 返回此套接字的输出流。- 如果此
Scoket
具有相关联的通道,则生成的OutputStream
的所有操作也关联该通道。 - 关闭生成的
OutputStream
也将关闭相关的Socket
。
- 如果此
public void close()
:关闭此套接字。- 一旦一个
Socket
被关闭,它将不可再使用。 - 关闭此
Socket
也将关闭相关的InputStream
和OutputStream
。
- 一旦一个
public void shutdownOutput()
: 禁用此套接字的输出流。- 任何先前写出的数据将被发送,随后终止输出流。
ServerSocket类
ServerSocket
类:实现了服务器套接字,该对象等待通过网络的请求。
构造方法
ServerSocket
类的构造方法如下:
public ServerSocket(int port)
:使用该构造方法在创建ServerSocket
对象时,可以将其绑定到一个指定的端口号(port
)上。
示例代码:1
ServerSocket server = new ServerSocket(6666);
成员方法
ServerSocket
类的成员方法如下:
public Socket accept()
:侦听并接受连接,返回一个新的Socket
对象,用于和客户端实现通信。该方法会一直阻塞直到客户端和服务端建立连接。
一个简单的TCP例子
服务端代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import ...
public class TCPServer {
public static void main(String[] args) throws IOException {
//创建绑定到特定端口的服务器套接字。
ServerSocket serverSocket = new ServerSocket(8888);
//调用服务器端的套接字对象的accept()方法接收客户端套接字(开启一个线程)
Socket socket = serverSocket.accept();
//获取客户端的输入流
InputStream inputStream = socket.getInputStream();
byte[] buf = new byte[1024];
int len = inputStream.read(buf);
System.out.println(new String(buf, 0, len));
//创建客户端的输出流,将数据写回给客户端
OutputStream outputStream = socket.getOutputStream();
outputStream.write("服务端收到了,你好呀,客户端!".getBytes());
socket.close();
serverSocket.close();
}
}
客户端代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import ...
public class TCPClient {
public static void main(String[] args) throws IOException {
//创建客户端套接字
Socket socket = new Socket("127.0.0.1",8888);
//创建客户端输出流
OutputStream outputStream = socket.getOutputStream();
outputStream.write("TCP服务器你好,我是客户端".getBytes());
//创建客户端输入流,接收服务器的客户端套接字的输出数据
InputStream inputStream = socket.getInputStream();
byte[] data = new byte[1024];
int len = inputStream.read(data);
System.out.println(new String(data,0,len));
socket.close();
}
}
分析及图示:
①【服务端】启动,创建ServerSocket
对象,等待连接(阻塞中)。
②【客户端】启动,创建Socket
对象,请求连接。
③【服务端】接收连接,调用accept
方法,并返回一个Socket
对象。
④【客户端】Socket
对象获取OutputStream
用来向服务端写出数据。
⑤【服务端】Scoket
对象获取InputStream
用来读取客户端发送的数据。
⑥【服务端】Socket
对象,获取OutputStream
,向客户端回写数据。
⑦【客户端】Scoket
对象,获取InputStream
,解析回写数据。
⑧【客户端】释放资源,断开连接。
运行结果如下:1
2
3
4//服务端接收到的消息
TCP服务器你好,我是客户端
//客户端接受到的消息
服务端收到了,你好呀,客户端!
例子:文件上传
客户端代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import ...
public class Client {
public static void main(String[] args) throws Exception {
System.out.println("客户端启动,准备上传文件");
//1.创建客户端Socker
Socket socket = new Socket("127.0.0.1", 8888);
//2.要上传的文件
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("/Users/apple/Downloads/test.jpg"));
//3.读取上传的文件并上传到服务器
OutputStream os = socket.getOutputStream();
byte[] buf = new byte[1024];
int length = 0;
while ((length = bis.read(buf)) != -1) {
os.write(buf, 0, length);
}
System.out.println("文件发送完毕");
//4.关闭流
os.close();
bis.close();
}
}
服务器代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import ...
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("服务器启动,等待客户端上传文件");
// 1. 创建服务端ServerSocket
ServerSocket serverSocket = new ServerSocket(8888);
// 2. 建立连接
Socket socket = serverSocket.accept();
// 3.创建上传文件夹
File file = new File("/Users/apple/Downloads/upload");
if (!file.exists()) {
file.mkdir();
}
// 4.读取上传的文件数据
InputStream is = socket.getInputStream();
byte[] buf = new byte[1024];
int length = 0;
// 5.将读取的文件存储在指定的文件夹并随机生成名字
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file + "/2019.jpg"));
while ((length = is.read(buf)) != -1) {
bos.write(buf, 0, length);
}
System.out.println("文件已存储完成");
//6。关闭流
bos.close();
is.close();
}
}
文件上传优化分析
①文件名称写死的问题:服务端,保存文件的名称如果写死,那么最终导致服务器硬盘,只会保留一个文件,建议使用系统时间优化,保证文件名称唯一,代码如下:
1 | FileOutputStream fis = new FileOutputStream(System.currentTimeMillis()+".jpg") // 文件名称 |
②循环接收的问题:服务端仅保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环改进,可以不断的接收不同用户的文件,代码如下:
1 | // 每次接收新的连接,创建一个Socket |
③效率问题:服务端,在接收大文件时,可能耗费几秒钟的时间,此时不能接收其他用户上传,所以可以使用多线程技术优化,代码如下:
1 | while(true){ |
改进的文件上传代码:
客户端代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import ...
public class Client {
public static void main(String[] args) throws Exception {
System.out.println("客户端启动,准备上传文件");
//1.创建客户端Socker
Socket socket = new Socket("127.0.0.1", 8888);
//2.要上传的文件
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("/Users/apple/Downloads/test.jpg"));
//3.读取上传的文件并上传到服务器
OutputStream os = socket.getOutputStream();
byte[] buf = new byte[1024];
int length = 0;
while ((length = bis.read(buf)) != -1) {
os.write(buf, 0, length);
}
System.out.println("文件发送完毕");
//4.关闭流
os.close();
bis.close();
}
}
服务器代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46public class Server {
public static void main(String[] args) throws Exception {
System.out.println("服务器启动,等待客户端上传文件");
// 1. 创建服务端ServerSocket
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
// 2. 建立连接
Socket socket = serverSocket.accept();
new Thread(() -> {
// 3.创建上传文件夹
File file = new File("/Users/apple/Downloads/upload");
if (!file.exists()) {
file.mkdir();
}
// 4.读取上传的文件数据
InputStream is = null;
BufferedOutputStream bos = null;
try {
is = socket.getInputStream();
byte[] buf = new byte[1024];
int length = 0;
// 5.将读取的文件存储在指定的文件夹并随机生成名字
bos = new BufferedOutputStream(new FileOutputStream(file + "/" + System.currentTimeMillis() + ".jpg"));
while ((length = is.read(buf)) != -1) {
bos.write(buf, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//6。关闭流
if (is != null) {
is.close();
}
if (bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("文件已存储完成");
}).start();
}
}
}
运行结果,不仅相应的目录生成文件,控制台还输出下面的代码:1
2
3
4
5
6
7
8
9//服务器
服务器启动,等待客户端上传文件
//客户端
客户端启动,准备上传文件
文件发送完毕
//服务
文件已存储完成
UDP通信(略)
参考
- 维基百科