【Java基础】【java.net】网络编程

网络编程概述

基本的通信架构

基本的通信架构有2种形式:CS架构( Client客户端/Server服务端 ) 、 BS架构(Browser浏览器/Server服务端)。

无论是CS架构,还是BS架构的软件都必须依赖网络编程!

CS架构

例如微信、IDEA。

Client客户端

  • 需要程序员开发。
  • 用户需要安装。

Server服务端

  • 需要程序员开发实现。

BS架构

例如浏览器、网页。

Browser浏览器

  • 不需要程序员开发实现。
  • 用户需要安装浏览器。

Server服务端

  • 需要程序员开发实现。

网络编程三要素

  1. IP地址:设备在网络中的地址,是唯一的标识。
  2. 端口号:应用程序在设备中唯一的标识。
  3. 协议:连接和数据在网络中传输的规则。

image-20240409203035483

IP地址

设备在网络中的地址,是唯一的标识。

  • IP(Internet Protocol):全称“互联网协议地址”,是分配给上网设备的唯一标志
  • IP地址有两种形式:IPv4、IPv6。
    • IPv4地址是用32bit(4Bytes)来表示,分成4段表示,使用点分十进制表示法来表示。
    • IPv6地址是用共128bit来表示,分成8段表示,使用冒分十六进制表示法来表示。即,每段每四位编码成一个十六进制位表示, 数之间用冒号(:)分开。

IP域名:

例如,https://www.baidu.com/。本机先去DNS服务器查,查到ip地址并返回,如果查不到,就去运营商服务器那里去查。

image-20240410104003448

公网IP、内网IP:

  • 公网IP:是可以连接互联网的IP地址;内网IP:也叫局域网IP,只能组织机构内部使用
  • 192.168. 开头的就是常见的局域网地址,范围即为192.168.0.0–192.168.255.255,专门为组织机构内部使用。

特殊IP地址:

  • 127.0.0.1、localhost:代表本机IP,只会寻找当前所在的主机。

IP常用命令:

  • ipconfig:查看本机IP地址
  • ping IP地址:检查网络是否连通

在Java中的代表类:

  • 代表IP地址。

InetAddress的常用方法:

  1. public static InetAddress getLocalHost()。获取本机IP,会以一个InetAddress的对象返回
  2. public static InetAddress getByName(String host)。根据ip地址或者域名返回一个InetAdress对象
  3. public String getHostName()。获取该ip地址对象对应的主机名
  4. public String getHostAddress()。获取该ip地址对象中的ip地址信息
  5. public boolean isReachable(int timeout)。在指定毫秒内,判断主机与该ip对应的主机是否能连通
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class InetAddressTest {
public static void main(String[] args) throws Exception {
// 1、获取本机IP地址对象的
InetAddress ip1 = InetAddress.getLocalHost();
System.out.println(ip1.getHostName());
System.out.println(ip1.getHostAddress());

// 2、获取指定IP或者域名的IP地址对象。
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());

// ping www.baidu.com
System.out.println(ip2.isReachable(6000));
}
}

端口号

应用程序设备唯一的标识。标记正在计算机设备上运行的应用程序的,被规定为一个 16 位的二进制,范围是 0~65535。

分类

  • 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用 80,FTP占用21)
  • 注册端口:1024~49151,分配给用户进程或某些应用程序。(注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。)
  • 动态端口:49152到65535,之所以称为动态端口,是因为它 一般不固定分配某种进程,而是动态分配。

协议

连接和数据在网络中传输的规则。网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。(为了让全球所有的上网设备都能够互联。)

开放式网络互联标准:OSI网络参考模型

  • OSI网络参考模型:全球网络互联标准。
  • TCP/IP网络模型:事实上的国际标准。

image-20240410105817922

传输层的2个通信协议

  • UDP(User Datagram Protocol):用户数据报协议。
  • TCP(Transmission Control Protocol):传输控制协议。

UDP协议

  • 特点:无连接、不可靠通信
  • 不事先建立连接,数据按照包发,一包数据包含:自己的IP、程序端口,目的地IP、程序端口和数据限制在64KB内)等。
  • 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的 。(但是通信效率高,用于语音通话、视频直播。)

TCP协议

  • 特点:面向连接、可靠通信
  • TCP的最终目的:要保证在不可靠的信道上实现可靠的传输。
  • TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接。(通信效率相对不高,用于网页、文件下载、支付。)
  • 可以进行大数据量的传输。

TCP协议:三次握手建立可靠连接

  • 可靠连接:确定通信双方,收发消息都是正常无问题的!(全双工)

image-20240410110231817

  • 传输数据会进行确认,以保证数据传输的可靠性。

TCP协议:四次握手断开连接

  • 目的:确保双方数据的收发都已经完成!

image-20240410110434580

UDP通信代码(入门案例)

  • 特点:无连接、不可靠通信。
  • 不事先建立连接;发送端每次把要发送的数据(限制在64KB内)、接收端IP、等信息封装成一个数据包,发出去就不管了。(类似于拿盘子扔韭菜……)
  • Java提供了一个java.net.DatagramSocket类来实现UDP通信。

DatagramSocket: 用于创建客户端、服务端

构造器

  1. public DatagramSocket()。创建客户端的Socket对象, 系统会随机分配一个端口号。
  2. public DatagramSocket(int port)。创建服务端的Socket对象, 并指定端口号。

方法

  1. public void send(DatagramPacket dp)。发送数据包。
  2. public void receive(DatagramPacket p)。使用数据包接收数据。

DatagramPacket:创建数据包

构造器

  1. public DatagramPacket(byte[] buf, int length, InetAddress address, int port)。创建发出去的数据包对象
  2. public DatagramPacket(byte[] buf, int length)。创建用来接收数据的数据包。

方法

  1. public int getLength()。获取数据包,实际接收到的字节个数。

使用UDP通信实现:发送消息、接收消息

客户端实现步骤

  1. 创建DatagramSocket对象(客户端对象) –> 扔韭菜的人。
  2. 创建DatagramPacket对象封装需要发送的数据(数据包对象) –> 韭菜盘子。
  3. 使用DatagramSocket对象的send方法,传入DatagramPacket对象 –> 开始抛出韭菜。
  4. 释放资源。
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
/**
* 目标:完成UDP通信快速入门:实现1发1收。
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建客户端对象(发韭菜出去的人)
DatagramSocket socket = new DatagramSocket(7777);

// 2、创建数据包对象封装要发出去的数据(创建一个韭菜盘子)
/* public DatagramPacket(byte buf[], int length,
InetAddress address, int port)
参数一:封装要发出去的数据。
参数二:发送出去的数据大小(字节个数)
参数三:服务端的IP地址(找到服务端主机)
参数四:服务端程序的端口。
*/
byte[] bytes = "我是快乐的客户端,我爱你abc".getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length
, InetAddress.getLocalHost(), 6666);

// 3、开始正式发送这个数据包的数据出去了
socket.send(packet);

System.out.println("客户端数据发送完毕~~~");
socket.close(); // 释放资源!
}
}

服务端实现步骤

  1. 创建DatagramSocket对象并指定端口(服务端对象) –> 接韭菜的人。
  2. 创建DatagramPacket对象接收数据(数据包对象) –> 韭菜盘子。
  3. 使用DatagramSocket对象的receive方法,传入DatagramPacket对象 –> 开始接收韭菜。
  4. 释放资源。
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
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1、创建一个服务端对象(创建一个接韭菜的人) 注册端口
DatagramSocket socket = new DatagramSocket(6666);

// 2、创建一个数据包对象,用于接收数据的(创建一个韭菜盘子)
byte[] buffer = new byte[1024 * 64]; // 64KB.
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

// 3、开始正式使用数据包来接收客户端发来的数据
socket.receive(packet);

// 4、从字节数组中,把接收到的数据直接打印出来
// 接收多少就倒出多少
// 获取本次数据包接收了多少数据。
int len = packet.getLength();

String rs = new String(buffer, 0 , len);
System.out.println(rs);

System.out.println(packet.getAddress().getHostAddress());
System.out.println(packet.getPort());

socket.close(); // 释放资源
}
}

UDP通信代码(多发多收)

客户端可以反复发送数据

客户端实现步骤

  • 创建DatagramSocket对象(发送端对象) –> 扔韭菜的人。
  • 使用while死循环不断的接收用户的数据输入,如果用户输入的exit则退出程序。
  • 如果用户输入的不是exit, 把数据封装成DatagramPacket –> 韭菜盘子。
  • 使用DatagramSocket对象的send方法将数据包对象进行发送 –> 开始抛出韭菜。
  • 释放资源。
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
/**
* 目标:完成UDP通信快速入门:实现客户端反复的发。
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建客户端对象(发韭菜出去的人)
DatagramSocket socket = new DatagramSocket();

// 2、创建数据包对象封装要发出去的数据(创建一个韭菜盘子)
/* public DatagramPacket(byte buf[], int length,
InetAddress address, int port)
参数一:封装要发出去的数据。
参数二:发送出去的数据大小(字节个数)
参数三:服务端的IP地址(找到服务端主机)
参数四:服务端程序的端口。
*/
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = sc.nextLine();

// 一旦发现用户输入的exit命令,就退出客户端
if("exit".equals(msg)){
System.out.println("欢迎下次光临!退出成功!");
socket.close(); // 释放资源
break; // 跳出死循环
}

byte[] bytes = msg.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length
, InetAddress.getLocalHost(), 6666);

// 3、开始正式发送这个数据包的数据出去了
socket.send(packet);
}
}
}

服务端实现步骤

  • 创建DatagramSocket对象并指定端口(接收端对象)–> 接韭菜的人。
  • 创建DatagramPacket对象接收数据(数据包对象) –> 韭菜盘子。
  • 使用DatagramSocket对象的receive方法传入DatagramPacket对象。
  • 使用while死循环不断的进行第3步。
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
/**
* 目标:完成UDP通信快速入门-服务端反复的收
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1、创建一个服务端对象(创建一个接韭菜的人) 注册端口
DatagramSocket socket = new DatagramSocket(6666);

// 2、创建一个数据包对象,用于接收数据的(创建一个韭菜盘子)
byte[] buffer = new byte[1024 * 64]; // 64KB.
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

while (true) {
// 3、开始正式使用数据包来接收客户端发来的数据
socket.receive(packet);

// 4、从字节数组中,把接收到的数据直接打印出来
// 接收多少就倒出多少
// 获取本次数据包接收了多少数据。
int len = packet.getLength();

String rs = new String(buffer, 0 , len);
System.out.println(rs);

System.out.println(packet.getAddress().getHostAddress());
System.out.println(packet.getPort());
System.out.println("--------------------------------------");
}
}
}

提问:UDP的接收端为什么可以接收很多发送端的消息?

  • 接收端只负责接收数据包,无所谓是哪个发送端的数据包。

TCP通信(一发一收)

  • 特点:面向连接、可靠通信。
  • 通信双方事先会采用“三次握手”方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端。
  • Java提供了一个java.net.Socket类来实现TCP通信。

TCP通信之-客户端开发

  • 客户端程序就是通过java.net包下的Socket类来实现的。

构造器

  1. public Socket(String host , int port)。根据指定的服务器ip、端口号请求与服务端建立连接,连接通过,就获得了客户端socket。

常用方法

  1. public OutputStream getOutputStream()。获得字节输出流对象。(发)
  2. public InputStream getInputStream()。获得字节输入流对象。(收)

客户端发送消息

  1. 创建客户端的Socket对象,请求与服务端的连接。
  2. 使用socket对象调用getOutputStream()方法得到字节输出流。(注意用的是字节流,所以使用的时候可能需要包装一下。)
  3. 使用字节输出流完成数据的发送。
  4. 释放资源:关闭socket管道。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 目标:完成TCP通信快速入门-客户端开发:实现1发1收。
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket对象,并同时请求与服务端程序的连接。
Socket socket = new Socket("127.0.0.1", 8888);

// 2、从socket通信管道中得到一个字节输出流,用来发数据给服务端程序。
OutputStream os = socket.getOutputStream();

// 3、把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);

// 4、开始写数据出去了
dos.writeUTF("在一起,好吗?");
dos.close();

socket.close(); // 释放连接资源
}
}

TCP通信-服务端程序的开发

  • 服务端是通过java.net包下的ServerSocket类来实现的。

构造器

  1. public ServerSocket(int port)。为服务端程序注册端口。

常用方法

  1. public Socket accept()。阻塞等待客户端的连接请求,一旦与某个客户端成功连接,则返回服务端这边的Socket对象。

实现步骤

  1. 创建ServerSocket对象,注册服务端端口。
  2. 调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象。
  3. 通过Socket对象调用getInputStream()方法得到字节输入流、完成数据的接收。
  4. 释放资源:关闭socket管道。
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
/**
* 目标:完成TCP通信快速入门-服务端开发:实现1发1收。
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8888);

// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();

// 3、从socket通信管道中得到一个字节输入流。
InputStream is = socket.getInputStream();

// 4、把原始的字节输入流包装成数据输入流
DataInputStream dis = new DataInputStream(is);

// 5、使用数据输入流读取客户端发送过来的消息
String rs = dis.readUTF();
System.out.println(rs);
// 其实我们也可以获取客户端的IP地址
System.out.println(socket.getRemoteSocketAddress());

dis.close();
socket.close();
}
}

TCP通信(多发多收)

  • 客户端使用死循环,让用户不断输入消息。
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
/**
* 目标:完成TCP通信快速入门-客户端开发:实现客户端可以反复的发消息出去
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket对象,并同时请求与服务端程序的连接。
Socket socket = new Socket("127.0.0.1", 8888);

// 2、从socket通信管道中得到一个字节输出流,用来发数据给服务端程序。
OutputStream os = socket.getOutputStream();

// 3、把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);

Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = sc.nextLine();

// 一旦用户输入了exit,就退出客户端程序
if("exit".equals(msg)){
System.out.println("欢迎您下次光临!退出成功!");
dos.close();
socket.close();
break;
}

// 4、开始写数据出去了
dos.writeUTF(msg);
dos.flush();
}
}
}
  • 服务端也使用死循环,控制服务端收完消息,继续等待接收下一个消息。
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
/**
* 目标:完成TCP通信快速入门-服务端开发:实现服务端反复发消息
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8888);

// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();

// 3、从socket通信管道中得到一个字节输入流。
InputStream is = socket.getInputStream();

// 4、把原始的字节输入流包装成数据输入流
DataInputStream dis = new DataInputStream(is);

while (true) {
try {
// 5、使用数据输入流读取客户端发送过来的消息
String rs = dis.readUTF();
System.out.println(rs);
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "离线了!");
dis.close();
socket.close();
break;
}
}
}
}

TCP通信(多线程改进)

目前我们开发的服务端程序,可否支持与多个客户端同时通信 ?

  • 不可以的。因为服务端现在只有一个主线程,只能处理一个客户端的消息。

image-20240410150207421

本次是如何实现服务端同时接收多个客户端的消息的?

  • 主线程定义了循环负责接收客户端Socket管道连接
  • 接收到一个Socket通信管道分配一个独立的线程负责处理它。

首先,我们需要写一个服务端的读取数据的线程类,代码如下

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
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true){
try {
String msg = dis.readUTF();
System.out.println(msg);

} catch (Exception e) {
System.out.println("有人下线了:" + socket.getRemoteSocketAddress());
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

接下来,再改写服务端的主程序代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 目标:完成TCP通信快速入门-服务端开发:要求实现与多个客户端同时通信。
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8888);

while (true) {
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();

System.out.println("有人上线了:" + socket.getRemoteSocketAddress());

// 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
new ServerReaderThread(socket).start();
}
}
}

拓展案例-【群聊的实现】-思路

客户端和客户端是不能直接通信的。全群聊的效果必须要有服务端在中间做中转。

  • 客户端 —> 服务端。
  • 客户端 —> 多个客户端。

image-20240410150623261

  • 是指一个客户端把消息发出去,其他在线的全部客户端都可以收到消息。
  • 需要用到端口转发的设计思想
  • 服务端需要把在线的Socket管道存储起来,一旦收到一个消息要推送给其他管道。

可以在服务端创建一个存储Socket的集合,每当一个客户端连接服务端,就可以把客户端Socket存储起来;当一个客户端给服务端发消息时,再遍历集合通过每个Socket将消息再转发给其他客户端。

然后改造服务端代码

由于服务端读取数据是在线程类中完成的,所以我们改SerReaderThread类就可以了。服务端的主程序不用改。

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
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true){
try {
String msg = dis.readUTF();
System.out.println(msg);
// 把这个消息分发给全部客户端进行接收。
sendMsgToAll(msg);
} catch (Exception e) {
System.out.println("有人下线了:" + socket.getRemoteSocketAddress());
Server.onLineSockets.remove(socket);
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

private void sendMsgToAll(String msg) throws IOException {
// 发送给全部在线的socket管道接收。
for (Socket onLineSocket : Server.onLineSockets) {
OutputStream os = onLineSocket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF(msg);
dos.flush();
}
}
}

BS架构程序(简易版)

案例:

要求从浏览器中访问服务器,并立即让服务器响应一个很简单的网页给浏览器展示,网页内容就是“黑马程序员666”。

BS架构的基本原理

  • 客户端使用浏览器发起请求(不需要开发客户端)。
  • 注意:服务器必须给浏览器响应HTTP协议规定的数据格式,否则浏览器不识别返回的数据。
  • http://服务器IP:服务器端口,例如,http://127.0.0.1:8080。

image-20240410151156205

HTTP协议规定

必须满足如下形式:

image-20240410151235576

注意:数据是由多行组成的,必须按照规定的格式来写。

先写一个线程类,用于按照HTTP协议的格式返回数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
// 立即响应一个网页内容:“黑马程序员”给浏览器展示。
try {
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=UTF-8");
ps.println(); // 必须换行
ps.println("<div style='color:red;font-size:120px;text-align:center'>黑马程序员666<div>");
ps.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

再写服务端的主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 目标:完成TCP通信快速入门-服务端开发:要求实现与多个客户端同时通信。
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8080);

while (true) {
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();

System.out.println("有人上线了:" + socket.getRemoteSocketAddress());

// 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
new ServerReaderThread(socket).start();
}
}
}

拓展

每次请求都开一个新线程,到底好不好?

  • 高并发时,容易宕机!
  • 可以使用线程池进行优化。

image-20240410151505454

为了避免服务端创建太多的线程,可以把服务端用线程池改进,提高服务端的性能。

先写一个给浏览器响应数据的线程任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
// 立即响应一个网页内容:“黑马程序员”给浏览器展示。
try {
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=UTF-8");
ps.println(); // 必须换行
ps.println("<div style='color:red;font-size:120px;text-align:center'>黑马程序员666<div>");
ps.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

再改写服务端的主程序,使用ThreadPoolExecutor创建一个线程池,每次接收到一个Socket就往线程池中提交任务就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8080);

// 创建出一个线程池,负责处理通信管道的任务。
ThreadPoolExecutor pool = new ThreadPoolExecutor(16 * 2, 16 * 2, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(8) , Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());

while (true) {
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();

// 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
pool.execute(new ServerReaderRunnable(socket));
}
}
}