十三、网络编程
什么是网络编程?
- 网络编程可以让程序与网络上的其他设备中的程序进行数据交互。
网络通信基本模式:
- 常见的通信模式有如下两种形式:Client/Server(CS模式)、Browser/Server(BS模式)。
- Client/Server(CS模式):客户端/服务端。
- 需要程序员单独开发客户端和服务端。
- 用户需要安装客户端。
- Browser/Server(BS模式):浏览器/服务器。
- 浏览器不需要程序员开发,需要开发服务器。
- 用户需要安装浏览器。
关于网络编程同学们需要学会什么?
- 网络通信的三要素:一个消息发送给对方需要哪些关键因素。
- UDP通信:消息直接发送给对方,不确认对方是否在线,不做消息确认。
- TCP通信:基于可靠传输的方式进行的通信模式。解决不同场景的通信需求。
- 即时通信:如何实现即时通信,具体是如何实现的。
- 模拟BS系统:Web系统是如何支持访问到网页的,具体是如何与服务器通信的。
1.网络通信三要素
实现网络编程关键的三要素:
-
IP地址:设备在网络中的地址,是唯一的标识。
-
端口:应用程序在设备中唯一的标识。
-
协议:数据在网络中传输的规则,常见的协议有UDP协议和TCP协议。
a.要素一:IP地址
IP地址:
- IP(Internet Protocol):全称”互联网协议地址“,是分配给上网设备的唯一标志。
- 常见的IP分类:IPv4、IPv6。
IPv4:
-
32bit(4字节)。
-
// 二进制 11000000 10101000 00000001 01000010 // 十进制 192.168.1.66
IPv4专用网络:
在IPv4所允许的大约四十亿地址中,三个地址块被保留作专用网络。这些地址块在专用网络之外不可路由,专用网络之内的主机也不能直接与公共网络通信。但通过网络地址转换(NAT),使用这些地址的主机可以像拥有共有地址的主机在互联网上通信。
名字 | 地址范围 | 地址数量 | 有类别的描述 | 最大的CIDR地址块 |
---|---|---|---|---|
24位块 | 10.0.0.0–10.255.255.255 | 16,777,216 | 一个A类 | 10.0.0.0/8 |
20位块 | 172.16.0.0–172.31.255.255 | 1,048,576 | 连续的16个B类 | 172.16.0.0/12 |
16位块 | 192.168.0.0–192.168.255.255 | 65,536 | 连续的256个C类 | 192.168.0.0/16 |
IPv6:
-
128位(16字节),号称可以为地址每一粒沙子编号。
-
IPv8分为8个整数,每个整数用四个十六进制表示,数之间用冒号分开。
-
// 二进制 1010101111001101 1110111100000001 10001101000101 110011110001001 1010101111001101 1110111100000001 10001101000101 110011110001001 // 十六进制 ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
IP常用命令:
- ipconfig/ifconfig:查看本机IP地址。
- ping IP地址:检查网络是否联通。
特殊IP地址:
- 本机IP地址:127.0.0.1或者localhost,称为回送地址或本地回环地址,只会寻找当前所在本机。
b.IP地址操作类:InetAddress
InetAddress的使用:
- 此类表示Internet协议(IP)地址。
InetAddress API如下:
名称 | 说明 |
---|---|
public static InetAddress getLocalHost() | 返回本主机的地址对象。 |
public static InetAddress getByName(String host) | 得到指定主机的IP地址对象,参数是域名或者IP地址。 |
public String getHostName() | 获取此IP地址的主机名。 |
publit Straing getHostAddress() | 返回IP地址字符串。 |
publit boolean isReachable(int timeout) | 在指定毫秒内连通该IP地址对应的主机,连通返回ture。 |
Test.java
import java.net.InetAddress;
import java.net.UnknownHostException;
public class Test {
public static void main(String[] args) throws Exception {
// 获取本机地址对象
InetAddress inetAddress = InetAddress.getLocalHost();
System.out.println(inetAddress.getHostName());
System.out.println(inetAddress.getHostAddress());
// 获取域名IP对象
InetAddress inetAddress1 = InetAddress.getByName("www.baidu.com");
System.out.println(inetAddress1.getHostName());
System.out.println(inetAddress1.getHostAddress());
// 获取公网IP对象
InetAddress inetAddress2 = InetAddress.getByName("14.215.177.39");
System.out.println(inetAddress2.getHostName());
System.out.println(inetAddress2.getHostAddress());
// 判断是否能通:ping 25秒内测试是否可通
System.out.println(inetAddress2.isReachable(25000));
}
}
c.要素二:端口号
端口号:
- 端口号:标识正在计算机设备上运行的进程(程序),被规定为一个16位的二进制,范围是0~65535。
端口类型:
- 保留端口:0~1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)。
- 注册端口:1024~49151,分配给用户进程或某些应用程序(如:Tomcat占用8080,MySQL占用3306)。
- 动态端口:49152~65535,一般不固定分配某种进程,而是动态分配。
注意:我们自己开发的程序选择注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。
按端口号可分为3大类:
(1)公认端口(Well Known Ports):从0到1023,它们紧密绑定(binding)于一些服务。通常这些端口的通讯明确表明了某种服务的协议。例如:80端口实际上总是HTTP通讯。
(2)注册端口(Registered Ports):从1024到49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。
(3)动态和/或私有端口(Dynamic and/or Private Ports):从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。但也有例外:SUN的RPC端口从32768开始。
d.要素三:协议
通信协议:
- 连接和通信数据的规则被称为网络通信协议。
网络通信协议有两套参考模型:
- OSI参考模型:世界互联协议标准,全球通信规范,由于此模型过于理想化,未能在因特网上进行广泛推广。
- TCP/IP参考模型(或TCP/IP协议):事实上的国际标准。
OSI参考模型 | TCP/IP参考模型 | 各层对应 | 面向操作 |
---|---|---|---|
应用层 | 应用层 | HTTP、FTP、DNS、SMTP… | 应用程序需要关注的浏览器,邮箱。 |
表示层 | |||
会话层 | |||
传输层 | 传输层 | TCP、UDP… | 选择使用的TCP、UDP协议。 |
网络层 | 网络层 | IP、ICMP… | 封装源和目标IP,进行路径选择。 |
数据链路层 | 数据链路层+物理层 | 物理寻址、比特流… | 物理设备中传输。 |
物理层 |
传输层的两个常见协议:
- TCP(Transmission Control Protocol):传输控制协议。
- UDP(User Datagram Protocol):用户数据报协议。
TCP协议特点:
- 使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议。
- 传输前,采用“三次握手”方式建立连接,所以是可靠的。
- 在连接中可进行大量数据的传输。
- 连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率较低。
TCP协议通信场景:
- 对信息安全要求较高的场景,例如:文件下载、金融等数据通信。
TCP三次握手:
TCP四次挥手断开连接:
UDP协议:
- UDP是一种无连接、不可靠传输的协议。
- 将数据源IP、目的地IP和端口封装成数据包,不需要建立连接。
- 每个数据包的大小限制在64kb内。
- 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的。
- 可以广播发送,发送数据结束时无需释放资源,开销小,速度快。
UDP协议通信场景:
- 语言通话、视频会话等。
2.UDP通信-快速入门
a.快速入门
UDP协议的特点:
- UDP是一种无连接、不可靠传输的协议。
- 将数据源IP、目的地IP和端口以及数据封装成数据包,大小限制在64kb内,直接发送出去即可。
DatagramPacket:数据包对象
构造器 | 说明 |
---|---|
public DatagramPacket(byte[] buf, int length, InetAddress address, int port) | 创建发送端数据包对象。 buf:要发送的内容,字节数组。 length:要发送的内容的字节长度。 address:接收端的IP地址对象。 port:接收端的端口号。 |
public DatagramPacket(byte[] buf, int length) | 创建接收端的数据包对象。 buf:用来存储接受的内容。 length:能够接受内容的长度。 |
DatagramSocket:发送端和接收端对象。
构造器 | 说明 |
---|---|
public DatagramSocket() | 创建发送端的Socket对象,系统会随机分配一个端口号。 |
public DatagramSocket(int port) | 创建接收端的Socket对象并指定端口号。 |
DatagramSocket类成员方法:
方法 | 说明 |
---|---|
public void send(DatagramPacket dp) | 发送数据包。 |
public void receive(DatagramPacket p) | 接收数据包。 |
案例:使用UDP通信实现:发送消息、接收消息
- 客户端实现步骤:
- 创建DatagramSocket对象(客户端)。
- 创建DatagramPacket对象封装需要发送的数据(数据包对象)。
- 使用DatagramSocket对象的send方法传入DatagramPacket对象。
- 释放资源。
- 服务端实现步骤:
- 创建DatagramSocket对象(服务端)。
- 创建DatagramPacket对象封装需要发送的数据(数据包对象)。
- 使用DatagramSocket对象的send方法传入DatagramPacket对象。
- 释放资源。
Server.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* 服务端
*/
public class Server {
public static void main(String[] args) throws Exception {
// 1.创建服务端对象
DatagramSocket socket = new DatagramSocket(8888);
// 2.创建一个数据包对象接收对象
byte[] bytes = new byte[1024 * 64];
// 接收数据包 接收数据包对象
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
// 3.等待接收数据
socket.receive(packet);
// 4.取出数据
// 获取接收数据的长度
int len = packet.getLength();
String rs = new String(bytes, 0, len);
System.out.println(rs);
// 获取客户端IP和端口
String ip = packet.getSocketAddress().toString();
System.out.println("客户端IP:" + ip);
String port = String.valueOf(packet.getPort());
System.out.println("客户端端口:" + port);
// 5.关闭服务端
socket.close();
}
}
Client.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* 客户端
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1.创建客户端对象 自带默认端口
DatagramSocket socket = new DatagramSocket();
// 2.创建一个数据包对象封装数据
// 要发送的数据
byte[] bytes = "我是1号,收到请回答!".getBytes();
// 要发送的数据 发送数据的大小 服务端IP地址 服务端端口
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 8888);
// 3.发送数据
socket.send(packet);
// 4.关闭客户端
socket.close();
}
}
需要先启动服务端再启动客户端。
b.多发多收
案例:使用UDP通信实现:多发多收消息。
需求:
- 使用UDP通信方式开发客户端和服务端。
分析:
- 客户端可以一直发送消息。
- 服务端可以不断的接收多个发送端的消息展示。
- 发送端输入了exit则结束发送端程序。
Server.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* 服务端 多收
*/
public class Server {
public static void main(String[] args) throws Exception {
// 1.创建服务端对象
DatagramSocket socket = new DatagramSocket(8888);
// 2.创建一个数据包对象接收对象
byte[] bytes = new byte[1024 * 64];
// 接收数据包 接收数据包对象
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
while (true) {
// 3.等待接收数据
socket.receive(packet);
// 4.取出数据
// 获取接收数据的长度
int len = packet.getLength();
// 获取客户端IP 端口 消息
String message = new String(bytes, 0, len);
String ip = packet.getSocketAddress().toString();
String port = String.valueOf(packet.getPort());
System.out.println("客户端IP:" + ip + "客户端端口:" + port + "消息:" + message);
}
// 5.关闭服务端
// socket.close();
}
}
Client.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
/**
* 客户端 多发
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1.创建客户端对象 自带默认端口
DatagramSocket socket = new DatagramSocket();
Scanner scanner = new Scanner(System.in);
while (true) {
// 2.创建一个数据包对象封装数据
// 要发送的数据
System.out.println("请输入:");
String sendMessage = scanner.nextLine();
if ("exit".equals(sendMessage)) {
System.out.println("离线成功!");
socket.close();
break;
}
byte[] bytes = sendMessage.getBytes();
// 要发送的数据 发送数据的大小 服务端IP地址 服务端端口
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 8888);
// 3.发送数据
socket.send(packet);
}
// 4.关闭客户端
socket.close();
}
}
执行结果:
客户端1
请输入:
111
请输入:
222
请输入:
333
请输入:
exit
离线成功!
客户端2
请输入:
aaa
请输入:
bbb
请输入:
ccc
请输入:
exit
离线成功!
服务端:
客户端IP:/127.0.0.1:54174客户端端口:54174消息:111
客户端IP:/127.0.0.1:50373客户端端口:50373消息:aaa
客户端IP:/127.0.0.1:54174客户端端口:54174消息:222
客户端IP:/127.0.0.1:50373客户端端口:50373消息:bbb
客户端IP:/127.0.0.1:54174客户端端口:54174消息:333
客户端IP:/127.0.0.1:50373客户端端口:50373消息:ccc
......
3.UDP通信-广播、组播
UDP的三种通信方式:
- 单播:单台主机与单台主机之间的通信。
- 广播:当前主机与所在网络中的所有主机通信。
- 组播:当前主机与选定的一组主机的通信。
UDP如何实现广播:
- 使用广播地址:255.255.255.255。
- 具体操作:
- 发送端发送的数据包的目的地写的是广播地址,且指定端口。(255.255.255.255, 9999)
- 本机所在网段的其他主机的程序只要匹配端口成功即就可以收到消息了。(9999)
Server.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* 服务端 多收
*/
public class Server {
public static void main(String[] args) throws Exception {
// 1.创建服务端对象
DatagramSocket socket = new DatagramSocket(9999);
// 2.创建一个数据包对象接收对象
byte[] bytes = new byte[1024 * 64];
// 接收数据包 接收数据包对象
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
while (true) {
// 3.等待接收数据
socket.receive(packet);
// 4.取出数据
// 获取接收数据的长度
int len = packet.getLength();
// 获取客户端IP 端口 消息
String message = new String(bytes, 0, len);
String ip = packet.getSocketAddress().toString();
String port = String.valueOf(packet.getPort());
System.out.println("客户端IP:" + ip + "端口:" + port + "消息:" + message);
}
// 5.关闭服务端
// socket.close();
}
}
Client.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
/**
* 客户端 多发
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1.创建客户端对象 自带默认端口
DatagramSocket socket = new DatagramSocket();
Scanner scanner = new Scanner(System.in);
while (true) {
// 2.创建一个数据包对象封装数据
// 要发送的数据
System.out.println("请输入:");
String sendMessage = scanner.nextLine();
if ("exit".equals(sendMessage)) {
System.out.println("离线成功!");
socket.close();
break;
}
byte[] bytes = sendMessage.getBytes();
// 要发送的数据 发送数据的大小 服务端IP地址 服务端端口
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("255.255.255.255"), 9999);
// 3.发送数据
socket.send(packet);
}
// 4.关闭客户端
socket.close();
}
}
执行结果:
发送端
请输入:
本条信息为广播信息。
请输入:
exit
离线成功!
接收端
客户端IP:/192.168.71.128:51198端口:51198消息:本条信息为广播信息。
UDP如何实现实现组播:
- 使用组播地址:224.0.0.0~239.255.255.255
- 具体操作:
- 发送端的数据包的目的地是组播IP(例如:224.0.1.1,端口9999)。
- 接收端必须绑定改组播IP(224.0.1.1),端口还要对应发送端的目的端口9999,这样即可接收该组播消息。
- DatagramSocket的子类MulticastSocket可以在接收端绑定组播IP。
Server.java
import java.net.*;
/**
* 服务端 多收
*/
public class Server {
public static void main(String[] args) throws Exception {
// 1.创建服务端对象
MulticastSocket socket = new MulticastSocket(9999);
// 当前接收端加入到一个组播组中去:绑定对应的组播消息的IP
// 该方法已过时
//socket.joinGroup(InetAddress.getByName("224.0.1.1"));
socket.joinGroup(new InetSocketAddress(InetAddress.getByName("224.0.1.1"), 9999), NetworkInterface.getByInetAddress(InetAddress.getLocalHost()));
// 2.创建一个数据包对象接收对象
byte[] bytes = new byte[1024 * 64];
// 接收数据包 接收数据包对象
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
while (true) {
// 3.等待接收数据
socket.receive(packet);
// 4.取出数据
// 获取接收数据的长度
int len = packet.getLength();
// 获取客户端IP 端口 消息
String message = new String(bytes, 0, len);
String ip = packet.getSocketAddress().toString();
String port = String.valueOf(packet.getPort());
System.out.println("客户端IP:" + ip + "客户端端口:" + port + "消息:" + message);
}
// 5.关闭服务端
// socket.close();
}
}
Client.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
/**
* 客户端 多发
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1.创建客户端对象 自带默认端口
DatagramSocket socket = new DatagramSocket();
Scanner scanner = new Scanner(System.in);
while (true) {
// 2.创建一个数据包对象封装数据
// 要发送的数据
System.out.println("请输入:");
String sendMessage = scanner.nextLine();
if ("exit".equals(sendMessage)) {
System.out.println("离线成功!");
socket.close();
break;
}
byte[] bytes = sendMessage.getBytes();
// 要发送的数据 发送数据的大小 服务端IP地址 服务端端口
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("224.0.1.1"), 9999);
// 3.发送数据
socket.send(packet);
}
// 4.关闭客户端
socket.close();
}
}
执行结果:
发送端
请输入:
本次发送的消息为组播消息。
请输入:
exit
离线成功!
接收端
客户端IP:/192.168.71.128:48519客户端端口:48519消息:本次发送的消息为组播消息。
总结:
- 如何实现广播,具体怎么操作?
- 发送端目的IP使用广播IP:255.255.255.255 9999
- 所在网段的其他主机对应了端口(9999)即可接收消息。
- 如何实现组播,具体怎么操作?
- 发送端目的IP使用组播IP,且指定端口。
- 所在网段的其他主机注册了该组播IP和对应端口即可接收消息。
4.TCP通信-快速入门
TCP协议回顾:
- TCP是一种面向连接,安全、可靠的传输数据的协议。
- 传输前,采用“三次握手”方式,点对点通信,是可靠的。
- 在连接中可进行大数据量的传输。
a.编写客户端代码
Socket:
构造器 | 说明 |
---|---|
public Socket(String host,int port) | 创建发送端的Socket对象与服务端连接,参数为服务端程序的IP和端口。 |
注意:java.net.Socket类实现通信,底层使用TCP协议。
Socket类成员方法:
方法 | 说明 |
---|---|
OutputStream getOutputStream() | 获得字节输出流对象。 |
InputStream getInputStream() | 获得字节输入流对象。 |
客户端发送消息:
- 创建客户端的Socket对象,请求与服务端的连接。
- 使用Socket对象调用getOutputStream()方法得到字节输出流。
- 使用字节输出流完成数据的发送。
- 释放资源:关闭Socket管道。
Client.java
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
/**
* 客户端
* 目标:完成Socket网络编程入门案例的客户端开发 实现一发一收
*/
public class Client {
public static void main(String[] args) {
try {
// 1.创建客户端的Socket对象 请求与服务端的连接
Socket socket = new Socket("127.0.0.1", 7777);
// 2.使用Socket对象调用getOutputStream()方法得到字节输出流
OutputStream outputStream = socket.getOutputStream();
// 3.把低级的字节流包装成打印流
PrintStream printStream = new PrintStream(outputStream);
// 4.发送消息
printStream.println("TCP客户端请求连接, 是否允许连接?");
// 5.关闭资源 一般不关闭
// socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
b.编写服务端代码、原理分析
ServerSocket:
构造器 | 说明 |
---|---|
public ServerSocket(int port) | 注册服务端端口。 |
ServerSocket类成员方法:
方法 | 说明 |
---|---|
public Socket accpet() | 等待接收客户端的Socket通信连接,连接成功后返回Socket对象与客户端建立端到端通信。 |
Server.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务端
* 目标:开发Socket网络编程入门的服务端 实现接收消息
*/
public class Server {
public static void main(String[] args) {
System.out.println("===服务端口启动===");
try {
// 1.注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// 2.必须调用accept方法 等待接收客户端的Socket连接请求 建立Socket通信管道
Socket socket = serverSocket.accept();
// 3.从Socket通信管道中得到一个字节输入流
InputStream inputStream = socket.getInputStream();
// 4.把字节输入流包装成缓冲字节输入流进行消息的接收
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// 5.按照行读取消息
String message;
while ((message = bufferedReader.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + ":" + message);
}
// 客户端断开后 自动结束程序
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行结果:
===服务端口启动===
/127.0.0.1:39898:TCP客户端请求连接, 是否允许连接?
5.TCP通信-多发多收消息
需求:使用TCP通信方式实现多发多收消息。
- 可以使用死循环控制服务端接收完消息后继续等待接下一个消息。
- 客户端也可以使用死循环等待用户不断输入消息。
- 客户端一旦输入了exit,则关闭客户端程序,并释放资源。
Server.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务端
* 目标:开发Socket网络编程入门的服务端 实现接收多收消息
*/
public class Server {
public static void main(String[] args) {
System.out.println("===服务端启动===");
try {
// 1.注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// 2.必须调用accept方法 等待接收客户端的Socket连接请求 建立Socket通信管道
Socket socket = serverSocket.accept();
// 3.从Socket通信管道中得到一个字节输入流
InputStream inputStream = socket.getInputStream();
// 4.把字节输入流包装成缓冲字节输入流进行消息的接收
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// 5.按照行读取消息
String message;
while ((message = bufferedReader.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + ":" + message);
}
// 客户端断开后 自动结束程序
} catch (IOException e) {
e.printStackTrace();
}
}
}
Client.java
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端
* 目标:完成Socket网络编程入门案例的客户端开发 实现多发消息
*/
public class Client {
public static void main(String[] args) {
System.out.println("===客户端启动===");
try {
// 1.创建客户端的Socket对象 请求与服务端的连接
Socket socket = new Socket("127.0.0.1", 7777);
// 2.使用Socket对象调用getOutputStream()方法得到字节输出流
OutputStream outputStream = socket.getOutputStream();
// 3.把低级的字节流包装成打印流
PrintStream printStream = new PrintStream(outputStream);
// 4.发送消息
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入:");
String message = scanner.nextLine();
if ("exit".equals(message)) {
System.out.println("离线成功!");
break;
}
printStream.println(message);
printStream.flush();
}
// 5.关闭资源 一般不关闭
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行结果:
===客户端启动===
请输入:
大弦嘈嘈如急雨
请输入:
小弦切切如私语
请输入:
exit
离线成功!
===服务端启动===
/127.0.0.1:43732:大弦嘈嘈如急雨
/127.0.0.1:43732:小弦切切如私语
本案例实现了多发多收,那么是否可以同时接收多个客户端的消息?
- 不可以。
- 因为服务端现在只有一个线程,只能与一个客户端进行通信。
6.TCP通信-同时接受多个客户端信息
- 之前我们的通信是否可以同时与多个客户端通信,为什么?
- 不可以。
- 单线程每次只能处理一个客户端的Socket通信。
- 如何才能让服务端可以处理多个客户端的通信需求?
- 引入多线程。
ServerReaderThread.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* Socket读取类
*/
public class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 3.从Socket通信管道中得到一个字节输入流
InputStream inputStream = socket.getInputStream();
// 4.把字节输入流包装成缓冲字节输入流进行消息的接收
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// 5.按照行读取消息
String message;
while ((message = bufferedReader.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + ":" + message);
}
// 客户端断开连接后 线程结束
System.out.println(socket.getRemoteSocketAddress() + "下线了");
} catch (IOException e) {
e.printStackTrace();
}
}
}
Server.java
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务端
* 目标:实现服务端可以同时处理多个客户端的消息
*/
public class Server {
public static void main(String[] args) {
System.out.println("===服务端启动===");
try {
// 1.注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// a.定义一个死循环由主线负责不断的接收客户端的Socket连接
while (true) {
// 2.必须调用accept方法 等待接收客户端的Socket连接请求 建立Socket通信管道
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
// 创建Socket读取类对象并启动子线程
new ServerReaderThread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Client.java
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端
* 目标:完成Socket网络编程入门案例的客户端开发 实现多发消息
*/
public class Client {
public static void main(String[] args) {
System.out.println("===客户端启动===");
try {
// 1.创建客户端的Socket对象 请求与服务端的连接
Socket socket = new Socket("127.0.0.1", 7777);
// 2.使用Socket对象调用getOutputStream()方法得到字节输出流
OutputStream outputStream = socket.getOutputStream();
// 3.把低级的字节流包装成打印流
PrintStream printStream = new PrintStream(outputStream);
// 4.发送消息
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入:");
String message = scanner.nextLine();
if ("exit".equals(message)) {
System.out.println("离线成功!");
break;
}
printStream.println(message);
printStream.flush();
}
// 5.关闭资源 一般不关闭
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行结果:
客户端一
===客户端启动===
请输入:
你好呀!
请输入:
你还好吧?
请输入:
exit
离线成功!
客户端二
===客户端启动===
请输入:
hello!
请输入:
How are you!
请输入:
exit
离线成功!
服务端
===服务端启动===
/127.0.0.1:43012上线了
/127.0.0.1:44556上线了
/127.0.0.1:43012:你好呀!
/127.0.0.1:44556:hello!
/127.0.0.1:43012:你还好吧?
/127.0.0.1:44556:How are you!
/127.0.0.1:43012下线了
/127.0.0.1:44556下线了
总结:
- 本次是如何实现服务端接收多个客户端的消息的?
- 主线程定义循环负责接收客户端Socket管道连接。
- 每接收到一个Socket通信管道后分配一个独立的线程负责处理它。
7.TCP通信-使用线程池优化
- 目前的通信架构存在什么问题?
- 客户端与服务端的线程模型是:N-N的关系。
- 客户端并发越多,线程越多,系统瘫痪的越快。
引入线程池处理多个客户端消息。
ServerReaderRunnable.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* Socket读取线程池类
*/
public class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 3.从Socket通信管道中得到一个字节输入流
InputStream inputStream = socket.getInputStream();
// 4.把字节输入流包装成缓冲字节输入流进行消息的接收
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// 5.按照行读取消息
String message;
while ((message = bufferedReader.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + ":" + message);
}
// 客户端断开连接后 线程结束
System.out.println(socket.getRemoteSocketAddress() + "下线了");
} catch (IOException e) {
e.printStackTrace();
}
}
}
Server.java
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
/**
* 服务端
* 目标:使用线程池优化实现通信 实现服务端可以同时处理多个客户端的消息
*/
public class Server {
// 使用静态变量记住一个线程池对象
// 核心线程数3个
// 最多线程数5个
// 临时线程的最大存活时间6秒
// 任务队列大小为2
private static ExecutorService pool = new ThreadPoolExecutor(
3, 5,6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()
);
public static void main(String[] args) {
System.out.println("===服务端启动===");
try {
// 1.注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// a.定义一个死循环由主线负责不断的接收客户端的Socket连接
while (true) {
// 2.必须调用accept方法 等待接收客户端的Socket连接请求 建立Socket通信管道
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
// 创建Socket读取线程池类对象
Runnable target = new ServerReaderRunnable(socket);
// 在线程池中执行
pool.execute(target);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Client.java
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端
* 目标:完成Socket网络编程入门案例的客户端开发 实现多发消息
*/
public class Client {
public static void main(String[] args) {
System.out.println("===客户端启动===");
try {
// 1.创建客户端的Socket对象 请求与服务端的连接
Socket socket = new Socket("127.0.0.1", 7777);
// 2.使用Socket对象调用getOutputStream()方法得到字节输出流
OutputStream outputStream = socket.getOutputStream();
// 3.把低级的字节流包装成打印流
PrintStream printStream = new PrintStream(outputStream);
// 4.发送消息
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入:");
String message = scanner.nextLine();
if ("exit".equals(message)) {
System.out.println("离线成功!");
break;
}
printStream.println(message);
printStream.flush();
}
// 5.关闭资源 一般不关闭
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行结果(依次启动八个客户端,轮询输入消息):
===客户端启动===
请输入:
大家好,我是甲。
请输入:
exit
离线成功!
===客户端启动===
请输入:
大家好,我是乙。
请输入:
exit
离线成功!
===客户端启动===
请输入:
大家好,我是丙。
请输入:
exit
离线成功!
===客户端启动===
请输入:
大家好,我是丁。大家能够收到我的消息吗?
请输入:
我是丁。
请输入:
exit
离线成功!
===客户端启动===
请输入:
大家好,我是戊。大家能够收到我的消息吗?
请输入:
我是戊。
请输入:
exit
离线成功!
===客户端启动===
请输入:
大家好,我是己。大家能够收到我的消息吗?
请输入:
exit
离线成功!
===客户端启动===
请输入:
大家好,我是庚。大家能够收到我的消息吗?
请输入:
exit
离线成功!
===客户端启动===
请输入:
大家好,我是辛。大家能够收到我的消息吗?
请输入:
exit
离线成功!
===服务端启动===
/127.0.0.1:54898上线了
/127.0.0.1:54900上线了
/127.0.0.1:42856上线了
/127.0.0.1:42862上线了
/127.0.0.1:54898:大家好,我是甲。
/127.0.0.1:54900:大家好,我是乙。
/127.0.0.1:42856:大家好,我是丙。
/127.0.0.1:59298上线了
/127.0.0.1:58358上线了
/127.0.0.1:58358:大家好,我是己。大家能够收到我的消息吗?
/127.0.0.1:45978上线了
/127.0.0.1:45978:大家好,我是庚。大家能够收到我的消息吗?
/127.0.0.1:51684上线了
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.javase.tcpdemo3.ServerReaderRunnable@1b28cdfa rejected from java.util.concurrent.ThreadPoolExecutor@eed1f14[Running, pool size = 5, active threads = 5, queued tasks = 2, completed tasks = 0]
at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2065)
at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:833)
at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1365)
at com.javase.tcpdemo3.Server.main(Server.java:37)
/127.0.0.1:54898下线了
/127.0.0.1:42862:大家好,我是丁。大家能够收到我的消息吗?
/127.0.0.1:54900下线了
/127.0.0.1:59298:大家好,我是戊。大家能够收到我的消息吗?
/127.0.0.1:42862:我是丁。
/127.0.0.1:59298:我是戊。
/127.0.0.1:42856下线了
/127.0.0.1:42862下线了
/127.0.0.1:59298下线了
/127.0.0.1:58358下线了
/127.0.0.1:45978下线了
注意:
- 本次设计服务端核心线程为三个,前三个客户端请求正常连接,当第四个和第五客户端请求时会放置在任务队列中。
- 当第六个和第七个客户端请求时,因为任务队列已满,开始创建临时线程。
- 第八个客户端请求时因为线程数量达到最大、任务队列已满,所以开始拒绝客户端连接请求。
- 第一个和第二客户端请求断开后腾出两个线程空间,第四个和第五个客户端请求开始执行。
总结:
- 本次使用线程池的优势在哪里?
- 服务端可以复用线程处理多个客户端,可以避免系统瘫痪。
- 适合客户端通信时长较短的场景。
8.TCP通信实战案例-即时通信
- 即时通信是什么含义,要实现怎么样的设计?
- 即时通信,是指一个客户端的消息发送出去,其他客户端可以接收到。
- 之前我们的消息都是发送给服务端的。
- 即时通信需要进行端口转发的设计思想。
Server.java
import java.awt.print.Printable;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/**
* 服务端
* 目标:实现服务端可以同时处理多个客户端的消息
*/
public class Server {
// 定义一个静态List集合存储当前全部在线的Socket管道
public static List<Socket> allOlineSockets = new ArrayList<>();
public static void main(String[] args) {
System.out.println("===服务端启动===");
try {
// 1.注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// a.定义一个死循环由主线负责不断的接收客户端的Socket连接
while (true) {
// 2.必须调用accept方法 等待接收客户端的Socket连接请求 建立Socket通信管道
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
// 将Socket管道加入到集合中
allOlineSockets.add(socket);
// 创建Socket读取类对象并启动子线程
new ServerReaderThread(socket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Socket读取线程类
*/
class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 3.从Socket通信管道中得到一个字节输入流
InputStream inputStream = socket.getInputStream();
// 4.把字节输入流包装成缓冲字节输入流进行消息的接收
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// 5.按照行读取消息
String message;
while ((message = bufferedReader.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + ":" + message);
// 把消息进行端口转发给全部客户端Socket管道
sendMsgToAll(message);
}
// 客户端断开连接后 线程结束
System.out.println(socket.getRemoteSocketAddress() + "下线了");
// 将Socket管道从到集合中移出
Server.allOlineSockets.remove(socket);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 发送消息给所有人
*
* @param message 消息
*/
private void sendMsgToAll(String message) throws Exception {
for (Socket olineSocket : Server.allOlineSockets) {
if (!socket.equals(olineSocket)) {
PrintStream printStream = new PrintStream(olineSocket.getOutputStream());
printStream.println(message);
printStream.flush();
}
}
}
}
Client.java
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端
* 1.客户端发送消息
* 2.客户端随时可能需要收到消息
*/
public class Client {
public static void main(String[] args) {
System.out.println("===客户端启动===");
try {
// 1.创建客户端的Socket对象 请求与服务端的连接
Socket socket = new Socket("127.0.0.1", 7777);
// a.创建一个独立线程专门负责客户端读消息
new ClientReaderThread(socket).start();
// 2.使用Socket对象调用getOutputStream()方法得到字节输出流
OutputStream outputStream = socket.getOutputStream();
// 3.把低级的字节流包装成打印流
PrintStream printStream = new PrintStream(outputStream);
// 4.发送消息
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入:");
String message = scanner.nextLine();
if ("exit".equals(message)) {
System.out.println("离线成功!");
break;
}
printStream.println(message);
printStream.flush();
}
// 5.关闭资源 一般不关闭
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ClientReaderThread extends Thread {
private Socket socket;
public ClientReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 3.从Socket通信管道中得到一个字节输入流
InputStream inputStream = socket.getInputStream();
// 4.把字节输入流包装成缓冲字节输入流进行消息的接收
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// 5.按照行读取消息
String message;
while ((message = bufferedReader.readLine()) != null) {
System.out.println("收到消息:" + message);
System.out.println("请输入:");
}
} catch (IOException e) {
System.out.println("服务端把你踢出来群聊!");
}
}
}
执行结果:
===客户端启动===
请输入:
大家好,我是甲!
请输入:
收到消息:大家好,我是乙!
请输入:
很高兴认识大家!
请输入:
收到消息:欢迎!
请输入:
exit
离线成功!
服务端把你踢出来群聊!
===客户端启动===
请输入:
收到消息:大家好,我是甲!
请输入:
大家好,我是乙!
请输入:
收到消息:很高兴认识大家!
请输入:
欢迎!
请输入:
exit
离线成功!
服务端把你踢出来群聊!
===服务端启动===
/127.0.0.1:48480上线了
/127.0.0.1:48494上线了
/127.0.0.1:48494:大家好,我是甲!
/127.0.0.1:48480:大家好,我是乙!
/127.0.0.1:48494:很高兴认识大家!
/127.0.0.1:48480:欢迎!
/127.0.0.1:48494下线了
/127.0.0.1:48480下线了
总结:
- 即时通信是什么含义,要实现怎么样的设计?
- 即时通信是指一个客户端的消息发出去,其他客户端可以接收到。
- 即时通信需要进行端口转发的设计思想。
- 服务端需要把在线的Socket管道存储起来。
- 一旦收到一个消息要推送给其他管道。
9.TCP通信实战案例-模拟BS系统
- 之前的客户端都是什么样的?
- 其实就是CS架构,客户端需要我们自己开发实现的。
- BS架构是怎么样的,需要开发客户端吗?
- 浏览器访问服务端,不需要开发客户端。
实现BS开发:
注意:服务器必须给浏览器响应HTTP协议格式的数据,否则浏览器不识别。
HTTP请求报文结构:
HTTP响应报文结构:
HTTP报文语法:
BrowserServer.java
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 多线程实现
*/
public class BrowserServer {
public static void main(String[] args) {
try {
// 1.注册端口
ServerSocket serverSocket = new ServerSocket(8080);
// 2.循环接收多个客户端请求
while (true) {
Socket socket = serverSocket.accept();
// 3.交给一个独立线程处理请求
new ServerReaderThread(socket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 创建打印流对象
PrintStream printStream = new PrintStream(socket.getOutputStream());
// 响应头
printStream.println("HTTP/1.1 200 OK");
printStream.println("Content-Type:text/html;charset=UTF-8");
printStream.println();
// 正文
printStream.println("<h1>模拟BS系统</h1>");
// 关闭流
printStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行程序后,在浏览器中访问:
http://127.0.0.1:8080/
模拟BS系统
BrowserServer.java
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
/**
* 线程池实现
*/
public class BrowserServer {
private static ExecutorService pool = new ThreadPoolExecutor(
3, 5,6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()
);
public static void main(String[] args) {
try {
// 1.注册端口
ServerSocket serverSocket = new ServerSocket(8080);
// 2.循环接收多个客户端请求
while (true) {
Socket socket = serverSocket.accept();
// 创建Socket读取线程池类对象
Runnable target = new ServerReaderRunnable(socket);
// 在线程池中执行
pool.execute(target);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Socket读取线程池类
*/
class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 创建打印流对象
PrintStream printStream = new PrintStream(socket.getOutputStream());
// 响应头
printStream.println("HTTP/1.1 200 OK");
printStream.println("Content-Type:text/html;charset=UTF-8");
printStream.println();
// 正文
printStream.println("<h1>模拟BS系统</h1>");
// 关闭流
printStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行程序后,在浏览器中访问:
http://127.0.0.1:8080/
模拟BS系统
总结:
- TCP通信如何实现B/S请求网页信息回来?
- 客户端使用浏览器发起请求(无需开发客户端)。
- 服务端必须按照浏览器的协议规则响应数据。
- 浏览器使用HTTP协议。