0、概述
- 网络编程可以让程序与网络上的其他设备中的程序进行数据交互。
- 网络通信的基本模式
- C/S:客户端,服务端模式
- B/S:浏览器,服务端模式
一、网络通信三要素
- IP地址:设备在网络中的地址,是唯一的标识
- 端口:应用程序在设备中唯一的标识
- 协议:数据在网络中传输的规则,常见的协议有UDP协议和TCP协议
1.1 IP地址
- IP(Internet Protocol):全称”互联网协议地址“,是分配给上网设备的唯一标志
- 常见IP分类:IPv4(32位)和IPv6(128位)
- IP地址形式
- 公网地址和私有地址(局域网使用)
- 192.168开头的就是常见的局域网地址
- IP常用命令
- ipconfig:查看本机IP地址
- ping IP:检查网络是否连通
- 特殊IP地址:127.0.0.1
1.2 IP地址操作类InetAddress
- 此类表示Internet协议(IP)地址
- InetAddress API
- public static InetAddress getLocalHost():返回本主机的地址对象
- public static InetAddress getByName(String host):得到指定主机的IP地址对象,参数是域名或者IP地址
- public string getHostName():获取此IP地址的主机名
- public String getHostAddress():返回IP地址字符串
- public boolean isReachable(int timeout):在指定毫秒内连通该IP地址对应的主机
public class inetAddressDemo1 {
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对象
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());
// 3、获取公网IP
InetAddress ip3 = InetAddress.getByName("112.80.248.76");
System.out.println(ip3.getHostName());
System.out.println(ip3.getHostAddress());
// 4、判断是否能通: ping 5s
System.out.println(ip3.isReachable(5000));
}
}
1.3 端口号
- 端口号:标识正在计算机设备上运行的进程(程序),被规定为一个16位的二进制,范围是0-65535
- 端口类型
- 周知端口:0-1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)
- 注册端口:1024-49151,分配给用户进程或某些应用程序(如:Tomcat占用8080,MySQL占用3306)
- 动态端口:49152到65535,之所以被称为动态端口,是因为它一般不固定分配某种进程,而是动态分配
1.4 协议
- 通信协议:连接和通信数据的规则被称为网络通信协议
- 网络通信协议模型
- OSI
- TCP/IP
- 传输层的2个常见协议
- TCP(Transmission Control Protocol):传输控制协议
- UDP(User Datagram Protocol):用户数据报协议
1.4.1 TCP协议
- TCP协议特点
- 使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议
- 传输前,采用“三次握手”方式建立连接,所以是可靠的
- 在连接中可进行大数据量的传输
- 连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率很低
- TCP协议通信场景:对信息安全较高的场景,例如:文件下载、金融等数据通信
- TCP三次握手确立连接
- 客户端向服务器发出连接请求,等待服务器确认
- 服务器向客户端放回一个响应,告诉客户端收到了请求
- 客户端向服务器再次发出确认信息,连接建立
- TCP四次挥手断开连接
- 客户端向服务器发出取消连接请求
- 服务器向客户端返回一个响应,表示收到客户端取消请求
- 服务器向客户端发出确认取消信息
- 客户端再次发送确认信息,连接取消
1.4.2 UDP协议
- UDP通信特点:
- UDP是一种无连接、不可靠传输协议
- 将数据源IP、目的地IP和端口封装成数据包,不需要建立连接
- 每个数据包的大小限制在64KB内
- 发送方不管对方是否准备好,接收方收到也不需要确认,故是不可靠的
- 可以广播发送,发送数据结束时无需释放资源,开销小,速度快
- UDP通信场景:语音通话,视频会话等
二、UDP通信-快速入门
2.1 UDP通信:快速入门
- DatagramPacket:数据包对象
- public DatagramPacket(byte[] buf,int length,InetAddress address,int port):创建发送数据包对象
- buff:要发送的内容,字节数组
- length:要发送内容的字节长度
- address:接收端的IP地址对象
- port:接收端的端口号
- public DatagramPacket(byte[] buf,int length):创建接收端的数据包对象
- buf:用来存储接收的内容
- length:能够接收内容的长度
- public DatagramPacket(byte[] buf,int length,InetAddress address,int port):创建发送数据包对象
- DatagramSocket:发送端和接收端对象
- public DatagramSocket():创建发送端的Socket对象,系统会随机分配一个端口号
- public DatagramSocket(int port):创建接收端的Socket对象并指定端口号
- public void send(DatagramPacket dp):发送数据包
- public void receive(DatagramPacket p):接收数据包
Server.java
/**
* 接收端
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("=====服务端启动=====");
// 1、创建接收端对象
DatagramSocket socket = new DatagramSocket(8888);
// 2、创建一个数据包对象接收数据
byte[] buffer = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
// 3、等待接收数据
socket.receive(packet);
// 4、取出数据
String rs = new String(buffer);
System.out.println(rs);
// 5、关闭接收端对象
socket.close();
}
}
Client.java
/**
* 发送端
*/
public class Client {
public static void main(String[] args) throws Exception {
System.out.println("=====客户端启动=====");
// 1、创建发送端对象:发送端自带默认端口号
DatagramSocket socket = new DatagramSocket();
// 2、创建一个数据包对象封装数据
byte[] buffer = "raining".getBytes();
DatagramPacket packet = new DatagramPacket(buffer,buffer.length, InetAddress.getLocalHost(),8888);
// 3、发送数据出去
socket.send(packet);
// 4、关闭发送端对象
socket.close();
}
}
先运行接收端,等待接收信息,再运行发送端
2.2 UDP通信:多发多收
- 需求:使用UDP通信方式开发接收端和发送端
- 发送端可以一直发送消息
- 接收端可以不断地接收多个发送端的消息展示
- 发送端输入了exit则结束发送端程序
- 客户端实现步骤:
- 创建DatagramSocket对象(发送端对象)
- 使用while死循环不断接收用户的数据输入,如果用户输入exit则退出程序
- 如果用户输入的不是exit,把数据封装成DatagramSocket
- 使用DatagramSocket对象的send方法将数据包对象发送
- 释放资源
- 服务端实现步骤:
- 创建DatagramSocket对象并指定端口(接收端对象)
- 创建DatagramPacket对象接收数据(数据包对象)
- 使用while循环,使用DatagramSocket对象的receive方法传入DatagramPacket对象
Server.java
/**
* 接收端 多发多收
*/
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];
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
while (true) {
// 3、等待接收数据
socket.receive(packet);
// 4、取出数据
String rs = new String(buffer);
System.out.println("收到了来自:" + packet.getAddress() + ":" + packet.getPort() + "的消息:" + rs);
}
}
}
Client.java
/**
* 发送端 多发多收
*/
public class Client {
public static void main(String[] args) throws Exception {
System.out.println("=====客户端启动=====");
// 1、创建发送端对象:发送端自带默认端口号
DatagramSocket socket = new DatagramSocket();
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入: ");
String msg = sc.nextLine();
if ("exit".equals(msg)) {
System.out.println("离线成功!");
socket.close();
break;
}
// 2、创建一个数据包对象封装数据
byte[] buffer = msg.getBytes();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getLocalHost(), 8888);
// 3、发送数据出去
socket.send(packet);
}
// 4、关闭发送端对象
socket.close();
}
}
可以开启多个进程使用多个端口接收信息
要配置Allow multiply instances
三、UDP通信-广播、组播
3.1 UDP的三种通信方式
- 单播:单台主机与单台主机之间的通信
- 广播:当前主机与所在网络中的所有主机通信
- 组播:当前主机与选定的一组主机的通信
3.2 UDP实现广播
- 使用广播地址:255.255.255.255
- 具体操作
- 发送端发送的数据包目的地址是广播地址,且指定端口。(255.255.255.255 9999)
- 本机所在网段的其他主机的程序只要匹配端口成功就可以收到消息了
Server.java
/**
* 接收端 广播
*/
public class Server {
public static void main(String[] args) throws Exception {
// 1、创建接收端对象,注册端口
DatagramSocket socket = new DatagramSocket(9999);
// 2、创建一个数据包对象接收数据
byte[] buffer = new byte[1024 * 64];
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);
String ip = packet.getSocketAddress().toString();
System.out.println("对方地址: " + ip);
int port = packet.getPort();
System.out.println("对方端口: " + port);
}
}
}
Client.java
/**
* 发送端 广播
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建发送端对象:发送端自带默认的端口号
DatagramSocket socket = new DatagramSocket(6666);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入: ");
String msg = sc.nextLine();
if ("exit".equals(msg)) {
System.out.println("离线成功!");
socket.close();
return;
}
// 2、创建一个数据包对象封装数据
byte[] buffer = msg.getBytes();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("255.255.255.255"), 9999);
// 3、发送数据
socket.send(packet);
}
}
}
3.3 UDP实现组播
- 使用组播地址:224.0.0.0~239.255.255.255
- 具体操作:
- 发送端的数据包的目的地址是组播IP(例如:224.0.1.1 9999)
- 接收端必须绑定该组播IP(224.0.1.1),端口还要对应发送端的目的端口9999,这样就可以接收该组播消息
- DatagramSocket的子类MulticastSocket可以在接收端绑定组播IP
Client.java
/**
* 发送端 广播
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建发送端对象:发送端自带默认的端口号
DatagramSocket socket = new DatagramSocket(6666);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入: ");
String msg = sc.nextLine();
if ("exit".equals(msg)) {
System.out.println("离线成功!");
socket.close();
return;
}
// 2、创建一个数据包对象封装数据
byte[] buffer = msg.getBytes();
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("224.0.1.1"), 9999);
// 3、发送数据
socket.send(packet);
}
}
}
Server.java
/**
* 接收端 广播
*/
public class Server {
public static void main(String[] args) throws Exception {
// 1、创建接收端对象,注册端口
MulticastSocket socket = new MulticastSocket(9999);
// 把当前接收端加入到一个组播组中;绑定对应组播消息的组播IP
socket.joinGroup(new InetSocketAddress(InetAddress.getByName("224.0.1.1"),9999),
NetworkInterface.ge tByInetAddress(InetAddress.getLocalHost()));
// 2、创建一个数据包对象接收数据
byte[] buffer = new byte[1024 * 64];
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);
String ip = packet.getSocketAddress().toString();
System.out.println("对方地址: " + ip);
int port = packet.getPort();
System.out.println("对方端口: " + port);
}
}
}
四、TCP通信-快速入门
4.1 编写客户端代码
- 客户端实现步骤
- 创建客户端的Socket对象,请求与服务端的连接
- 使用Socket对象调用getOutputStream()方法得到字节输出流
- 使用字节输出流完成数据的发送
- 释放资源:关闭socket管道
Client.java
/**
*Socket网络编程:TCP客户端开发
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket通信管道,请求与服务端连接
Socket socket = new Socket("127.0.0.1",7777);
// 2、从socket通信管道中得到一个字节输出流,负责发送数据
OutputStream os = socket.getOutputStream();
// 3、把低级的字节流包装成打印流
PrintStream ps = new PrintStream(os);
// 4、发送消息
ps.print("我是TCP的客户端,我已经与你对接,并发出邀请");
ps.flush();
}
}
4.2 编写服务端代码、原理分析
- public ServerSocket(int port):注册服务端端口
- public Socket accept():等待接收客户端的Socket通信连接,连接成功返回Socket对象与客户端建立端到端通信
- 服务端实现步骤
- 创建ServerSocket对象,注册服务端端口
- 调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象
- 通过Socket对象调用gentInputStream()方法得到字节输入流,完成数据的接收
- 释放资源:关闭socket管道
Server.java
/**
* Socket网络编程:TCP服务端开发
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("====服务端启动成功====");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// 2、调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
Socket socket = serverSocket.accept();
// 3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成缓冲字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照行读取消息
String msg;
if ((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
}
}
}
五、TCP通信-多发多收消息
- 实现步骤
- 使用死循环控制服务端收完消息继续等待接收下一个消息
- 客户端使用死循环等待用户不断输入消息
- 客户端输入exit,关闭客户端,并释放资源
Server.java
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("====服务端启动成功====");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// 2、调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
Socket socket = serverSocket.accept();
// 3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成缓冲字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照行读取消息
String msg;
while ((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
}
}
}
Client.java
/**
*Socket网络编程:TCP客户端开发
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket通信管道,请求与服务端连接
Socket socket = new Socket("127.0.0.1",7777);
// 2、从socket通信管道中得到一个字节输出流,负责发送数据
OutputStream os = socket.getOutputStream();
// 3、把低级的字节流包装成打印流
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入: ");
String msg = sc.nextLine();
// 4、发送消息
ps.println(msg);
ps.flush();
}
}
}
六、TCP通信-同时接受多个客户端消息
- 让服务端可以处理多个客户端的通信需求:引入多线程
- 主线程定义循环负责接收客户端Socket管道连接
- 每接收到一个Socket通信管道后分配一个独立的线程负责处理它
Server.java
/**
* Socket网络编程:TCP服务端可以同时处理多个客户端的消息
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("====服务端启动成功====");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// a.定义一个死循环由主线程负责不断接收客户端的Socket管道连接
while (true) {
// 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
// 3、开始创建独立线程处理socket
new ServerReaderThread(socket).start();
}
}
}
Client.java
/**
*Socket网络编程:TCP服务端可以同时处理多个客户端的消息
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket通信管道,请求与服务端连接
Socket socket = new Socket("127.0.0.1",7777);
// 2、从socket通信管道中得到一个字节输出流,负责发送数据
OutputStream os = socket.getOutputStream();
// 3、把低级的字节流包装成打印流
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入: ");
String msg = sc.nextLine();
// 4、发送消息
ps.println(msg);
ps.flush();
}
}
}
ServerReaderThread.java
public class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成缓冲字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照行读取消息
String msg;
while ((msg = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
}
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "下线了!");
}
}
}
七、TCP通信-使用线程池优化
- 上一个案例中,客户端与服务端的线程模型是:N-N的关系
- 引入线程池处理多个客户端的消息
Server.java
/**
* 使用线程池优化,实现通信
*/
public class Server {
// 使用静态变量记住一个线程池对象
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) throws Exception {
System.out.println("====服务端启动成功====");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// a.定义一个死循环由主线程负责不断接收客户端的Socket管道连接
while (true) {
// 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
// 3、开始创建独立线程处理socket
ServerReaderRunnable target = new ServerReaderRunnable(socket);
pool.execute(target);
}
}
}
Client.java
/**
* 使用线程池优化,实现通信
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket通信管道,请求与服务端连接
Socket socket = new Socket("127.0.0.1",7777);
// 2、从socket通信管道中得到一个字节输出流,负责发送数据
OutputStream os = socket.getOutputStream();
// 3、把低级的字节流包装成打印流
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入: ");
String msg = sc.nextLine();
// 4、发送消息
ps.println(msg);
ps.flush();
}
}
}
ServerReaderRunnable
public class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
// 3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成缓冲字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照行读取消息
String msg;
while ((msg = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
}
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "下线了!");
}
}
}
- 服务端可以复用线程处理多个客户端,可以避免系统瘫痪
- 适合客户端通信时长较短的场景
八、TCP通信实战案例-即时通信
8.1 即时通信概述
- 即时通信,是指一个客户端的消息发出去,其他客户端可以接收到
- 即时通信需要端口转发的思想
- 服务器需要把在线的Socket管道存储起来
- 一旦受到一个消息要推送给所有管道
Server.java
/**
*客户端发送消息
*随时可能收到消息
*/
public class Server {
// 定义一个静态的List集合存储当前全部在线的socket管道,方便以后为他们转发消息
public static List<Socket> allOnlineSockets = new ArrayList<>();
public static void main(String[] args) throws Exception {
System.out.println("====服务端启动成功====");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// a.定义一个死循环由主线程负责不断接收客户端的Socket管道连接
while (true) {
// 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
allOnlineSockets.add(socket);
// 3、开始创建独立线程处理socket
new ServerReaderThread(socket).start();
}
}
}
Client.java
/**
*客户端发送消息
*随时可能收到消息
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket通信管道,请求与服务端连接
Socket socket = new Socket("127.0.0.1",7777);
// 创建一个独立的线程专门负责这个客户端的读消息
new ClientReaderThread(socket).start();
// 2、从socket通信管道中得到一个字节输出流,负责发送数据
OutputStream os = socket.getOutputStream();
// 3、把低级的字节流包装成打印流
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入: ");
String msg = sc.nextLine();
// 4、发送消息
ps.println(msg);
ps.flush();
}
}
}
class ClientReaderThread extends Thread{
private Socket socket;
public ClientReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成缓冲字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照行读取消息
String line;
while ((line = br.readLine()) != null) {
System.out.println("收到消息:" + line);
}
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "服务端把你踢出群聊");
}
}
}
ServerReaderThread.java
public class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成缓冲字符输入流进行消息的接收
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 5、按照行读取消息
String msg;
while ((msg = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
// 把消息进行端口转发给全部客户端socket管道,使用输出流
sendMsgToAll(msg);
}
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "下线了!");
Server.allOnlineSockets.remove(socket);
}
}
private void sendMsgToAll(String msg) throws Exception {
for (Socket socket : Server.allOnlineSockets) {
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println(msg);
ps.flush();
}
}
九、TCP通信—BS系统
- 客户端使用浏览器发起请求(不需要开发客户端)
- 服务端必须按照浏览器的协议规则响应数据
- HTTP协议