网络编程
1、软件架构
- C/S结构:全称为Client/Server结构,是指客户端和服务器结构。
- B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。
两种结构各有优势,但是无论哪种结构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序。
-
网络通信协议:通信协议是对计算机必须遵守的规则,只有遵守这些规则,计算机之间才能进行通信。这就好 比在道路中行驶的汽车一定要遵守交通规则一样,协议中对数据的传输格式、传输速率、传输步骤等做了统一 规定,通信双方必须同时遵守,最终完成数据交换。
-
TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),是Internet 最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包 含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。
通信的协议还是比较复杂的, java.net 包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些 类和接口,来专注于网络程序开发,而不用考虑通信的细节。
java.net 包中提供了两种常见的网络协议的支持:
- TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在 发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。 第三次握手,客户端再次向服务器端发送确认信息,确认连接。
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以 保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。
- UDP:用户数据报协议(User Datagram Protocol)。UDP协议是一个面向无连接的协议。传输数据时,不需要 建立连接,不管对方端服务是否启动,直接将数据、数据源和目的地都封装在数据包中,直接发送。每个数据 包的大小限制在64k以内。它是不可靠协议,因为无连接,所以传输速度快,但是容易丢失数据。日常应用中, 例如视频会议、QQ聊天等。 每次发送的数据最大为64kb
2、网络通信三要素
协议
协议:计算机网络通信必须遵守的规则,已经介绍过了,不再赘述。
IP地址
指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备 做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。
- IPv4:是一个32位的二进制数,通常被分为4个字节,表示成 a.b.c.d 的形式,例如 192.168.65.100 。其中 a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。
- IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。有 资料显示,全球IPv4地址在2011年2月分配完毕。 为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制 数,表示成 ABCD:EF01:2345:6789:ABCD:EF01:2345:6789 ,号称可以为全世界的每一粒沙子编上一个网址, 这样就解决了网络地址资源数量不够的问题。
- 特殊的IP地址
本机IP地址: 127.0.0.1 、 localhost 。
端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这 些进程呢?
如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。
- 端口号:用两个字节表示的整数,它的取值范围是065535。其中,01023之间的端口号用于一些知名的网络 服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致 当前程序启动失败。
- 利用协议+ IP地址+端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它 进程进行交互。
TCP通信
TCP的客户端
在java中,有一个类,可以表示TCP的客户端,这个类叫做Socket
-
Socket构造方法:
Socket(String host, int port):
参数host表示目标服务器的IP地址。
参数port表示目标服务器程序的端口。 -
Socket的其他方法:
OutputStream getOutStream():获取输出流,用来发送数据。
InputStream getInputStream():获取输入流,用来接收数据。
void close():释放资源。 -
TCP客户端的实现步骤:
(1)创建Socket对象,并指定要连接的服务器的IP地址以及服务器程序的端口号。
(2)调用Socket的getOutputStream方法获取输出流,用来发送数据。
(3)调用输出流的write方法发送数据。
(4)调用Socket的getInputStream方法获取输入流,用来接收数据。
(5)调用输入流的方法接收数据。
(6)释放资源。
public class Demo01Client {
public static void main(String[] args) throws IOException {
//1. 创建Socket对象,并指定要连接的服务器的ip地址以及服务器程序的端口号。
//创建Socket对象的时候,会主动连接服务器,如果连接不成功,就会报错。三次握手就是在这一步发生的。
Socket socket = new Socket("127.0.0.1", 9527);
//2. 调用Socket的getOutputStream方法获取输出流,用来发送数据。
//获取到的输出流目的地是服务器
OutputStream out = socket.getOutputStream();
//3. 调用输出流的write方法发送数据。
out.write("你好".getBytes());
//4. 调用Socket的getInputStream方法获取输入流, 用来接收数据。
InputStream in = socket.getInputStream();
//5. 调用输入流的read方法接收数据。
byte[] bArr = new byte[1024];
int len = in.read(bArr);
System.out.println(new String(bArr, 0, len));
//6. 释放资源。
socket.close();
}
TCP案例的服务器的实现
在java中有一个类叫做ServerSocket,这个类表示服务器,我们可以使用这个类实现服务器程序。
-
ServerSocket的构造方法:
ServerSocket(int port):
参数要传递int类型的端口号,该端口号表示服务器程序自己的端口号。 -
ServerSocket的其他方法:
Socket accept():监听获取客户端Socket(客户端请求)。
void close();释放资源。 -
服务器程序的实现步骤:
(1)创建ServerSocket对象,表示服务器。
(2)调用服务器的accept方法,监听并获取客户端Socket。
(3)调用Socket对象的输入流,用来接收数据。
(4)调用输入流的read方法,去接收客户端发送过来的数据。
(5)通过Socket对象获取输出流,用来发送数据。
(6)调用输出流的write方法,给客户端发送数据。
(7)释放资源。
public class Demo02Server {
public static void main(String[] args) throws IOException {
//1. 创建ServerSocket对象,表示服务器。
ServerSocket serverSocket = new ServerSocket(9527);
//2. 调用服务器的accept方法,监听并获取客户端Socket。
Socket socket = serverSocket.accept();
//3. 调用Socket对象获取输入流,用来接收数据。
InputStream in = socket.getInputStream();
//4. 调用输入流的read方法,去接收客户端发送过来的数据。
byte[] bArr = new byte[1024];
int len = in.read(bArr);
System.out.println(new String(bArr, 0, len));
//5. 通过Socket对象获取输出流,用来发送数据。
OutputStream out = socket.getOutputStream();
//6. 调用输出流的write方法,给客户端发送数据。
out.write("收到!".getBytes());
//7. 释放资源
socket.close();
serverSocket.close();
}
文件上传案例
上传案例的客户端
对于客户端来说,要做的事情是读取自己电脑文件的字节,将这些字节写给服务器。还需要接收服务器发送过来的消息。
客户端的实现步骤:
1. 创建Socket客户端对象
2. 创建FileInputStream流,用来读取客户端自己电脑的文件。
3. 通过Socket获取OutputStream流, 用来向服务器写数据。
4. 开始读写,一次读写一个字节数组。 每读取到数据,就将读取到的数据写到服务器中。
5. 释放资源。
6. 通过Socket获取InputStream流, 用来读取服务器发送过来的数据。
7. 通过InputStream调用read方法读取数据
8. 释放资源
public class Demo01Client {
public static void main(String[] args) throws IOException {
//1. 创建Socket客户端对象
Socket socket = new Socket("127.0.0.1", 9527);
//2. 创建FileInputStream流,用来读取客户端自己电脑的文件。
InputStream is = new FileInputStream("d:\\client\\aa.jpg");
//3. 通过Socket获取OutputStream流, 用来向服务器写数据。
OutputStream out = socket.getOutputStream();
//4. 开始读写,一次读写一个字节数组。 每读取到数据,就将读取到的数据写到服务器中。
byte[] bArr = new byte[1024];
int len;
while ((len = is.read(bArr)) != -1) {
//如果条件成立,表示读取到了数据,那么就将读取到的数据写到服务器中
out.write(bArr, 0, len);
}
//告诉服务器,我已经操作完了,以后不会再给你写数据了,你也不要等我了
socket.shutdownOutput();
//5. 释放资源。
is.close();
//6. 通过Socket获取InputStream流, 用来读取服务器发送过来的数据。
InputStream in = socket.getInputStream();
//7. 通过InputStream调用read方法读取数据
len = in.read(bArr);
System.out.println(new String(bArr, 0, len));
//8. 释放资源
socket.close();
}
}
TCP上传案例的服务器
对于服务器来说,要做的事情是读取客户端发送过来的字节, 然后将这些字节写到自己电脑。 然后给客户端回复(上传成功)
实现步骤:
1. 创建ServerSocket表示服务器。
2. 调用ServerSocket的accept方法,监听获取客户端请求。
3. 调用Socket的getInputStream获取输入流,用来读取客户端发送过来的数据。
4. 创建FileOutputStream字节输出流,用来向服务器自己电脑写数据。
5. 开始读写, 一次读写一个字节数组。 每读取到数据,就将读取到的数据写到自己电脑。
6. 释放资源、
7. 调用Socket的getOutputStream获取输出流,用来给客户端发送数据。
8. 通过该输出流给客户端写数据。
9. 释放资源。
public class Demo02Server {
public static void main(String[] args) throws IOException {
//1. 创建ServerSocket表示服务器。
ServerSocket serverSocket = new ServerSocket(9527);
//2. 调用ServerSocket的accept方法,监听获取客户端请求。
Socket socket = serverSocket.accept();
//3. 调用Socket的getInputStream获取输入流,用来读取客户端发送过来的数据。
InputStream in = socket.getInputStream();
//4. 创建FileOutputStream字节输出流,用来向服务器自己电脑写数据。
//OutputStream os = new FileOutputStream("d:\\server\\aa.jpg");
//OutputStream os = new FileOutputStream("d:\\server\\" + System.currentTimeMillis() + ".jpg");
//有一个类叫做UUID,里面有一个方法叫做randomUUID可以获取一个随机不重复的字符序列,然后通过该字符序列调用toString方法可以得到字符串
OutputStream os = new FileOutputStream("d:\\server\\" + UUID.randomUUID().toString() + ".jpg");
//5. 开始读写, 一次读写一个字节数组。 每读取到数据,就将读取到的数据写到自己电脑。
byte[] bArr = new byte[1024];
int len;
while ((len = in.read(bArr)) != -1) {
//如果条件成立,就表示读取到了数据,就将读取到的数据写到服务器自己电脑
os.write(bArr, 0, len);
}
//6. 释放资源、
os.close();
//7. 调用Socket的getOutputStream获取输出流,用来给客户端发送数据。
OutputStream out = socket.getOutputStream();
//8. 通过该输出流给客户端写数据。
out.write("上传成功".getBytes());
//9. 释放资源。
socket.close();
serverSocket.close();
}
}
服务器改进版
-
之前的服务器接收一次上传请求后服务器就会停止。
-
改进: 服务器可以一直给客户端执行上传任务,如果第一个客户端上传完了,那么也可以继续给后面的客户端执行上传任务。
我们可以使用死循环, 在死循环中,让服务器一直监听客户端的请求,每监听到客户端的请求,那么就给该客户端执行上传任务。 -
死循环版本的服务器存在的问题: 如果有一个客户端上传了非常大的文件,服务器会一直给这个客户端执行上传操作,那么就无法监听下一个客户端的请求了。
-
问题原因:当程序启动JVM会创建main线程,执行main方法,main线程执行accept方法监听客户端的请求,如果有客户端发来请求了,main线程会向下执行,去该该客户端执行上传任务,如果该上传任务没有执行完,main线程无法向下继续执行, 就无法去监听下一个客户端的请求了。
- 解决方式: 使用多线程。 使用main线程监听客户的请求,如果有客户端来请求了,那么就创建一个新的线程,使用新的线程给该客户端执行上传操作。 这样新线程执行上传操作的同时不会影响到main线程,main线程可以继续往下执行,去监听下一个客户端的请求。
@SuppressWarnings("all")
public class Demo04ThreadServer {
public static void main(String[] args) throws IOException {
//1. 创建ServerSocket表示服务器。
ServerSocket serverSocket = new ServerSocket(9527);
//在死循环中,让服务器一直监听客户端的请求,每监听到客户端的请求,那么就给该客户端执行上传任务。
while (true) {
//2. 调用ServerSocket的accept方法,监听获取客户端请求。
Socket socket = serverSocket.accept();
//创建新的线程,给客户端执行上传任务
new Thread(() -> {
try {
//3. 调用Socket的getInputStream获取输入流,用来读取客户端发送过来的数据。
InputStream in = socket.getInputStream();
//4. 创建FileOutputStream字节输出流,用来向服务器自己电脑写数据。
OutputStream os = new FileOutputStream("d:\\server\\" + UUID.randomUUID().toString() + ".jpg");
//5. 开始读写, 一次读写一个字节数组。 每读取到数据,就将读取到的数据写到自己电脑。
byte[] bArr = new byte[1024];
int len;
while ((len = in.read(bArr)) != -1) {
//如果条件成立,就表示读取到了数据,就将读取到的数据写到服务器自己电脑
os.write(bArr, 0, len);
}
//6. 释放资源、
os.close();
//7. 调用Socket的getOutputStream获取输出流,用来给客户端发送数据。
OutputStream out = socket.getOutputStream();
//8. 通过该输出流给客户端写数据。
out.write("上传成功".getBytes());
//9. 释放资源。
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}