网络编程
可以让设备中的程序与网络上其他设备中的程序进行数据交互(实现网络通信)
基本的通信架构
- CS架构(Client客户端/Server服务端)
- BS架构(Browser浏览器/Server服务端)
无论是CS架构,还是BS架构的软件都必须依赖网络编程!
1. 网络通信三要素
1.1 IP地址
设备在网络中的地址,是唯一的标识
IP(Internet Protocol):全称”互联网协议地址”,是分配给上网设备的唯一标志
IP地址有两种形式:IPv4和IPv6
1.1.1 IPV4地址
1.1.2 IPv6地址
- IPv6:共128位,号称可以为地球每一粒沙子编号,即可以表示2的128次幂个编号
- IPv6分成8段表示,每段每四位编码成一个十六进制位表示,数之间用冒号(:)分开
1.1.3 IP域名
有一些并不是直接展示IP地址,而是展示IP域名,使用IP域名去代替IP地址,通过服务器解析IP域名就可以获取到IP地址
1.1.4 公网IP,内网IP
- 公网IP:是可以连接互联网的IP地址
- 内网IP:也叫局域网IP,只能组织机构内部使用
- 192.168.开头的就是常见的局域网地址,范围即为192.168.0.0–192.168.255.255,专门为组织机构内部使用
1.1.5 特殊IP地址
127.0.0.1、localhost:代表本机IP,只会寻找当前所在的主机
1.1.6 cmd中常见关于IP地址的命令
- ipconfig:查看本机IP地址
- ping IP地址:检查本机与该IP地址是否联通
1.1.7 Java中代表IP地址的对象InetAddress
方法名称 | 说明 |
---|---|
public static InetAddress getLocalHost( ) | 获取本机IP,封装在一个InetAddress对象中 |
public static InetAddress getByName( String host ) | 根据ip地址或者域名,返回一个InetAdress对象 |
public String getHostName( ) | 获取该InetAddress对象对应的主机名 |
public String getHostAddress( ) | 获取该InetAddress对象中的ip地址信息 |
public boolean isReachable( int timeout ) | 在指定毫秒内,判断主机与该InetAddress对象对应的主机是否能连通 |
public class Test1 {
public static void main(String[] args) throws Exception {
// 获取本机的IP地址对象
InetAddress ip1 = InetAddress.getLocalHost();
//获取主机名
String name = ip1.getHostName();
System.out.println("主机名:" + name);
//获取IP地址
String ip = ip1.getHostAddress();
System.out.println("IP地址:" + ip);
// 根据IP地址或者域名获取IP地址对象
InetAddress ip2 = InetAddress.getByName("www.bilibili.com");
System.out.println("主机名:" + ip2.getHostName());
System.out.println("IP地址:" + ip2.getHostAddress());
// 判断在指定毫秒内本机是否与该对象对应的ip地址连通
System.out.println(ip2.isReachable(1000));
}
}
1.2 端口号
-
应用程序在设备中唯一的标识
-
用于标记正在计算机设备上运行的应用程序,被规定为一个16位的二进制,范围是0~65535
1.2.1 端口分类
- 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)
- 注册端口:1024~49151,分配给用户进程或某些应用程序
- 动态端口:49152到65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配
注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错
1.3 通信协议
网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议
1.3.1 开放式网络参考标准:OSI网络参考模型
- OSI网络参考模型:全球网络互联标准
- TCP/IP网络模型:事实上的国际标准
1.3.2 传输层的两个通信协议
- UDP(User Datagram Protocol):用户数据报协议;
- TCP(Transmission Control Protocol):传输控制协议
1.3.3 UDP协议
- 特点:每次传输前不会建立连接、不可靠通信
- 优点:通信效率高,适应于语音通话、视频直播等
- 不事先建立连接,直接将数据按照包发送,一包数据包含:自己的IP、程序端口,目的地IP、程序端口和数据(限制在64KB内,超过则将超出部分重新打包发送)等。
- 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不需要返回确认,故是不可靠的。
1.3.4 TCP协议
- 特点:每次传输前都要建立连接、可靠通信
- 缺点:通信效率相对不高,使用于网页、文件下载、支付等
- 最终目的:保证在不可靠的信道上实现可靠的传输
- 实现可靠传输的三个步骤
- 三次握手建立连接
- 传输数据进行确认
- 四次挥手断开连接
(1) 三次握手建立连接
可靠连接:确定通信双方收发消息都是正常无问题的!(全双工)
第一次确定服务器端可以接受消息,第二次确定服务器端可以发送消息,第三次确定客户端可以接受和发送信息
(2)传输数据进行确认
如上图在成功建立连接后,才可以进行数据的传输。
每次传输数据进行确认,只有收到对方的确认消息才认为数据传输成功,否则认为数据传输失败,再重新向对方发送消息,知道对象发送确认收到信息,以此来保证数据传输的可靠性
(3)四次挥手断开连接
目的:确保双方数据都收发完成才断开连接
2. 实现UDP通信(Java.net.DatagramSocket)
2.1 实现UDP通信一发一收
2.1.1 DatagramSocket:用于创建客户端(发送端)和服务端(接收端)
构造器 | 说明 |
---|---|
public DatagramSocket( ) | 创建客户端的Socket对象,系统会随机分配一个端口号 |
public DatagramSocket( int port ) | 创建服务端的Socket对象,并指定端口号 |
方法 | 说明 |
---|---|
public void send(DatagramPacket dp) | 客户端发送数据包 |
public void receive(DatagramPacket p) | 服务端使用数据包接收数据 |
2.1.2 DatagramPacket:创建数据包
构造器 | 说明 |
---|---|
public DatagramPacket(byte[ ] buf, int length, InetAddress address, int port) | 创建发出去的数据包对象,封装客户端待发送数据,参数为待发送数据,数据长度,接收端对应的InetAddress对象和端口号 |
public DatagramPacket(byte[ ] buf, int length) | 创建用来接收数据的数据包 |
方法 | 说明 |
---|---|
public int getLength( ) | 获取数据包实际接收到的字节个数 |
2.1.3 实现
(1)客户端实现步骤
- 创建DatagramSocket对象(客户端对象)
- 创建DatagramPacket对象封装需要发送的数据(数据包对象)
- 使用DatagramSocket对象的send方法,传入DatagramPacket对象
- 释放资源
public class Client {
public static void main(String[] args) throws Exception {
//1.创建一个DatagramSocket客户端对象,随机端口号
DatagramSocket socket = new DatagramSocket();
//2.创建一个DatagramPacket对象,封装要发送的数据,接收端IP地址对象,端口号
/*
参数解析:
byte[] buf:发送的内容
int length:发送内容的长度
InetAddress address:服务端的IP地址对象
int port:接收端的端口号
*/
byte[] bytes = "发送的内容就是这个".getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 6666);
//3.调用DatagramSocket对象的方法send,向服务端发送数据
socket.send(packet);
System.out.println("发送成功");
//4.关闭资源
socket.close();
//需要先跑服务端程序再跑客户端程序,否则客户端发送的数据没有接收端接收
}
}
(2)服务端实现步骤
- 创建DatagramSocket对象并指定端口(服务端对象)
- 创建DatagramPacket对象接收数据(数据包对象)
- 使用DatagramSocket对象的receive方法,传入DatagramPacket对象
- 释放资源
public class Client {
public static void main(String[] args) throws Exception {
//1.创建一个DatagramSocket客户端对象,随机端口号
DatagramSocket socket = new DatagramSocket();
//2.创建一个DatagramPacket对象,封装要发送的数据,接收端IP地址对象,端口号
/*
参数解析:
byte[] buf:发送的内容
int length:发送内容的长度
InetAddress address:服务端的IP地址对象
int port:接收端的端口号
*/
byte[] bytes = "发送的内容就是这个".getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 6666);
//3.调用DatagramSocket对象的方法send,向服务端发送数据
socket.send(packet);
System.out.println("发送成功");
//4.关闭资源
socket.close();
//需要先跑服务端程序再跑客户端程序,否则客户端发送的数据没有接收端接收
}
}
(3)运行步骤
先运行服务端,在运行客户端,否则客户端发送数据无人接收
2.2 UDP通信多发多收
修改单发单收代码实现多发多收,只需要将单发单收的代码用死循环嵌套并简单修改即可
public class Client {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入要发送的内容");
String s = scanner.nextLine();
if (s.equals("exit")) {
System.out.println("程序退出");
socket.close();//关闭资源
break;//结束循环
}
byte[] bytes = s.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 6666);
socket.send(packet);
System.out.println("发送成功");
}
}
}
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动");
DatagramSocket socket = new DatagramSocket(6666);
byte[] buffer = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (true) {
socket.receive(packet);
int length = packet.getLength();//获取接收到的数据的长度
String result = new String(buffer, 0, length);//将接收到的数据转换为字符串
System.out.println(result);
InetAddress clientAddress = packet.getAddress();//获取发送端的IP地址对象
System.out.println(clientAddress.getHostAddress());//获取发送端的IP地址字符串
System.out.println(packet.getPort());//获取发送端的端口号
System.out.println("=================================");
}
//不需要关闭资源,因为需要一直监听,只有有内容就要一直接收,所以服务端不需要关闭资源
}
}
2.3 UDP通信同时接收多个客户端
idea中设置允许同时接受多个客户端的消息,无需修改多发多收的代码
因为idea一个程序只能跑一次,想要重复运行不关掉之前的程序如下设置,从而实现一个服务器同时接收多个不同端口的客户端的消息
结果如下:
图片过大无法上传
3. 实现TCP通信
- 特点:面向连接、可靠通信。
- 通信双方事先会采用“三次握手”方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端
3.1 TCP通信快速入门
3.1.1 客户端开发(Java.net.Socket)
构造器 | 说明 |
---|---|
public Socket(String host, int port) | 根据指定的服务器ip、端口号请求与服务端建立连接,连接通过,就获得了客户端的Socket对象 |
方法 | 说明 |
---|---|
public Outputstream getoutputstream( ) | 获取字节输出流对象 |
public Inputstream getInputstream( ) | 获取字节输入流对象 |
步骤
- 创建客户端对象Socket
- 获取输出流对象
- 包装输出流对象为高级的数据输出流对象
- 写出数据
- 关闭流
- 关闭客户端
public class Client {
public static void main(String[] args) throws Exception {
//1.创建客户端对象Socket
Socket socket = new Socket("127.0.0.1", 9999);//参数为服务器端的IP地址与端口号
//因为与本机建立通讯,所以传入的参数为本机IP地址和端口号
//2.获取输出流对象
OutputStream os = socket.getOutputStream();
//3.包装输出流对象为高级的数据输出流对象
DataOutputStream dos = new DataOutputStream(os);
//4.写出数据
dos.writeUTF("hello,server");
//5.关闭流
dos.close();
//6.关闭客户端
socket.close();
}
}
3.1.2 服务端开发(Java.net.ServerSocket)
构造器 | 说明 |
---|---|
public ServerSocket( ) | 为服务端注册端口 |
方法 | 说明 |
---|---|
public Socket accept( ) | 阻塞等待客户端的连接请求,一旦与某个客户端成功连接,则返回服务端与该客户端连通的Socket对象 |
步骤
- 创建服务器对象ServerSocket
- 调用accept()方法等待客户端的请求
- 从Socket对象中获取输入流对象
- 将原始的字节输入流包装成高级的数据输入流
- 读取数据
- 关闭流
- 关闭客户端
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("服务器端启动了...");
//1. 创建服务器对象ServerSocket
ServerSocket serverSocket = new ServerSocket(9999);
//2. 调用accept()方法等待客户端的请求
Socket socket = serverSocket.accept();//此时获取的是服务端的Socket对象
//3. 从Socket对象中获取输入流对象
InputStream is = socket.getInputStream();
//4. 将原始的字节输入流包装成高级的数据输入流
DataInputStream dis = new DataInputStream(is);
//5. 读取数据
String msg = dis.readUTF();
System.out.println(msg);
System.out.println(socket.getRemoteSocketAddress());//获取客户端的IP地址和端口号
//6. 关闭流
dis.close();
//7. 关闭客户端
socket.close();
}
}
3.2 TCP通信多发多收
改造上面的代码即可
3.2.1 客户端改造
public class Client {
public static void main(String[] args) throws Exception {
//1.创建客户端对象Socket
Socket socket = new Socket("127.0.0.1", 9999);//参数为本机IP地址和端口号
//2.获取输出流对象
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();
if (msg.equals("exit")) {
System.out.println("客户端退出...");
dos.close();
socket.close();
break;
}
dos.writeUTF(msg);
dos.flush();//刷新缓冲区
}
dos.close();
socket.close();
}
}
3.2.2 服务端改造
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("服务器端启动了...");
//1. 创建服务器对象ServerSocket
ServerSocket serverSocket = new ServerSocket(9999);
//2. 调用accept()方法等待客户端的请求
Socket socket = serverSocket.accept();//此时获取的是服务端的Socket对象
//3. 从Socket对象中获取输入流对象
InputStream is = socket.getInputStream();
//4. 将原始的字节输入流包装成高级的数据输入流
DataInputStream dis = new DataInputStream(is);
//5. 读取数据
//如果客户端关闭了,会抛出异常,所以需要进一步捕获异常
while (true) {
try {
String msg = dis.readUTF();
System.out.println(msg);
System.out.println("===================================");
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "客户端已经离线了");
dis.close();
socket.close();
break;
}
}
}
}
3.3 TCP通信同时接收多个客户端
不能像UDP通信一样直接多开之前的多个客户端,多开的客户端发送的消息无法被服务端接收,因为每个管道对应一个服务端与客户端的通信,所以只会接收第一个客户端的消息。(服务端现在只有一个主线程,只能处理一个客户端的消息)
3.3.1 客户端实现(无变化,多开即可)
public class Client {
public static void main(String[] args) throws Exception {
//1.创建客户端对象Socket
Socket socket = new Socket("127.0.0.1", 9999);//参数为本机IP地址和端口号
//2.获取输出流对象
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();
if (msg.equals("exit")) {
System.out.println("客户端退出...");
dos.close();
socket.close();
break;
}
dos.writeUTF(msg);
dos.flush();//刷新缓冲区
}
dos.close();
socket.close();
}
}
3.3.2 服务端实现
线程类
public class ServerReadThread extends Thread{
private Socket socket;
public ServerReadThread(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(socket.getRemoteSocketAddress() + "客户端发送:" + msg);
System.out.println("===================================");
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "客户端已经离线了");
dis.close();
socket.close();
break;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
服务端
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("服务器端启动了...");
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
Socket socket = serverSocket.accept();//此时获取的是服务端的Socket对象
System.out.println(socket.getRemoteSocketAddress()+"客户端已经连接上了");
new ServerReadThread(socket).start();
}
}
}
3.4 TCP通信综合案例
3.4.1 即时通信-群聊(客户端–》多个客户端)
由客户端发送给服务端,服务端再转发给其他客户端
(1)客户端
// 用于接收服务端所转发过来的消息的线程类
public class ClientReadThread extends Thread{
private Socket socket;
public ClientReadThread(Socket socket){
this.socket = socket;
}
// 用于接收服务端所转发过来的消息
@Override
public void run(){
try {
DataInputStream dis = new DataInputStream(socket.getInputStream());
while (true) {
try {
String msg = dis.readUTF();
System.out.println(msg);
System.out.println("===================================");
} catch (Exception e) {
System.out.println("当前客户端退出登录");
dis.close();
socket.close();
break;
}
}
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public class Client {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 9999);//参数为本机IP地址和端口号
System.out.println(socket.getLocalSocketAddress() + "客户端启动");
new ClientReadThread(socket).start();
OutputStream os = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入要发送的信息:");
String msg = sc.nextLine();
if (msg.equals("exit")) {
System.out.println("客户端退出...");
dos.close();
socket.close();
break;
}
dos.writeUTF(msg);
dos.flush();//刷新缓冲区
}
dos.close();
socket.close();
}
}
(2)服务端
public class ServerReadThread extends Thread{
private Socket socket;
public ServerReadThread(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();
//socket.getRemoteSocketAddress()获取另一端的的IP地址和端口号
//socket.getLocalSocketAddress()获取本端的IP地址和端口号
//将服务端收到的内容群发给所有在线的客户端,包括自己
sendMsgToAll(socket.getRemoteSocketAddress() + "发送:" + msg);
System.out.println(socket.getRemoteSocketAddress() + "客户端发送:" + msg);
System.out.println("===================================");
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "客户端已经离线了");
// 从在线socket集合中删除离线的socket
Server.onLineSockets.remove(socket);
dis.close();
socket.close();
break;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void sendMsgToAll(String msg) throws Exception{
// 发送给全部在线的socket管道接收。
for (Socket onLineSocket : Server.onLineSockets) {
if (onLineSocket != socket) {
//用于将服务端收到的内容群发给所有在线的客户端,不包括自己
OutputStream os = onLineSocket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF(msg);
dos.flush();
}
}
}
}
public class Server {
// 保存所有在线socket管道对象
public static List<Socket> onLineSockets = new ArrayList<>();
public static void main(String[] args) throws Exception {
System.out.println("服务器端启动了...");
ServerSocket serverSocket = new ServerSocket(9999);
while (true) {
Socket socket = serverSocket.accept();//此时获取的是服务端与接收到的客户端连通的Socket对象
System.out.println(socket.getRemoteSocketAddress()+"客户端已经连接上了");
onLineSockets.add(socket);
new ServerReadThread(socket).start();
}
}
}
3.4.2 实现一个简易版的BS架构
需求:要求从浏览器访问服务器并且让服务器相应一个简单的网页给浏览器,内容为“Hello World”
注意:服务器必须给浏览器相应HTTP协议规定的数据格式,否则浏览器无法识别
HTTP协议
(1)服务器每收到一个请求就为该请求创建一个线程
缺点:当访问量过大,即出现高并发时,服务器容易出现宕机
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("服务器端启动了...");
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();//此时获取的是服务端与接收到的客户端连通的Socket对象
System.out.println(socket.getRemoteSocketAddress()+"浏览器请求连接");
new ServerReadThread(socket).start();
}
}
}
public class ServerReadThread extends Thread{
private Socket socket;
public ServerReadThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
PrintStream ps = new PrintStream(socket.getOutputStream());
//想要网页的内容能够被识别的固定格式
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=utf-8");
ps.println();//空行,必须有
//下面才能写网页的内容
ps.println("<div style = 'color:red'>Hello World</div>");
//BS架构每次可以直接关闭socket,因为每次要想要给网页的内容只有一次
ps.close();
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
(2)使用线程池优化(解决宕机问题)
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("服务器端启动了...");
ServerSocket serverSocket = new ServerSocket(8080);
//创建线程池
ExecutorService pool = new ThreadPoolExecutor(20 * 2, 20 * 2, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(8), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
while (true) {
Socket socket = serverSocket.accept();//此时获取的是服务端与接收到的客户端连通的Socket对象
System.out.println(socket.getRemoteSocketAddress()+"浏览器请求连接");
//创建Runnable任务对象
Runnable target = new ServerReadRunnable(socket);
//将任务提交给线程池
pool.execute(target);
}
}
}
public class ServerReadRunnable implements Runnable{
private Socket socket;
public ServerReadRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
PrintStream ps = new PrintStream(socket.getOutputStream());
//想要网页的内容能够被识别的固定格式
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=utf-8");
ps.println();//空行,必须有
//下面才能写网页的内容
ps.println("<div style = 'color:red'>Hello World</div>");
//BS架构每次可以直接关闭socket,因为每次要想要给网页的内容只有一次
ps.close();
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}