文章目录
- 1. 概述
- 基本的通信架构 >>> 都依赖网络编程
- 2. 网络编程的三要素:IP、端口、协议
- 2.1 IP地址
- 1)IPV4
- 2)IPV6
- 3)IP域名(Domain Name) --- DNS域名解析器(Domain Name System)
- 4)公网IP、内网IP
- 5)InetAddress
- 2.2 端口
- 2.3 网络通信协议
- 3. ⭐传输层的2个通信协议
- 3.1 UDP(User Datagram Protocol):用户数据报协议
- 3.2 TCP(Transmission Control Protocol):传输控制协议
- 🤔三次握手建立连接:
- 🤔四次挥手断开连接:
- 4. UDP通信
- 4.1 一发一收
- 1) 客户端
- 2) 服务端
- 4.2 多发多收
- 1) 客户端
- 2) 服务端
- 3) 多次运行同一个java类
- 4) 结果
- 5. TCP通信
- 5.1 一发一收
- 1)客户端 Socket
- 2)服务端 ServerSocket
- 5.2 多发多收(客户端只有一个)
- 1)客户端
- 2)服务端
- 5.3 ⭐同时接收多个客户端的消息
- 1)接收数据的线程
- 2)服务端(主线程)
- 3)客户端
- 5.4 B/S架构的原理(网站开发)
- 1)服务端
- 2)使用线程池优化
网络编程 |
1. 概述
网络编程:
- 可以让设备中的程序与网络上其他设备中的程序进行数据交互的技术(实现网络通信)
基本的通信架构 >>> 都依赖网络编程
- CS结构(Client客户端/Server服务端)
- BS架构(Browser浏览器/Server服务端)
2. 网络编程的三要素:IP、端口、协议
2.1 IP地址
IP地址
设备在网络中的地址 >>> 设备在网络中的唯一标识
- 目前,使用广泛的IP地址形式:IPV4,IPV6
1)IPV4
Internet Protocol version 4的缩写,使用32位地址 — 点分十进制
2)IPV6
Internet Protocol version 6的缩写,使用128位地址 — 冒分十六进制
3)IP域名(Domain Name) — DNS域名解析器(Domain Name System)
域名:
- 用于在互联网上识别和定位网站的人类可读的名称
域名解析器:
- 用于将域名转换成对应的IP地址的分布式命名系统,将易记的域名映射到数字化的IP地址,使得用户可以通过域名来访问网站和其他网络资源
4)公网IP、内网IP
公网IP
- 可以连接到互联网的IP地址
内网IP
- 局域网IP,只能在组织机构内部使用的IP地址
本机IP
- 127.0.0.1、localhost
IP常用命令
- ipconfig:查看本机IP地址
- ping IP地址:检查网络是否连通
5)InetAddress
代表IP地址
// 目标:认识InetAddress获取本机IP对象和对方IP对象
try {// 1.1获取本机IP对象 (127.0.0.1)InetAddress local = InetAddress.getLocalHost();System.out.println(local); // 自己的IP地址// 1.2获取对方IP对象InetAddress remote = InetAddress.getByName("www.baidu.com");System.out.println(remote); // www.baidu.com/36.152.44.132// 1.3 判断主机在指定毫秒内与该ip的连接是否成功boolean reachable = remote.isReachable(3000);System.out.println(reachable); // true// 2.获取IP地址对应的主机名String hostName = remote.getHostName();System.out.println(hostName); // www.baidu.com// 3.获取IP地址对象中的IP地址信息String hostAddress = remote.getHostAddress();System.out.println(hostAddress); // 36.152.44.132} catch (Exception e) {e.printStackTrace();
}
2.2 端口
用来标记正在计算机设备上运行的应用程序(一个16位的二进制:0~65535) — 应用程序在设备中的唯一标识
分类:
2.3 网络通信协议
连接和数据在网络中传输的规则
3. ⭐传输层的2个通信协议
3.1 UDP(User Datagram Protocol):用户数据报协议
3.2 TCP(Transmission Control Protocol):传输控制协议
- 面向连接、可靠通信
- 最终目的:保证在不可靠的信道上实现可靠的数据传输
- 实现可靠传输的三个步骤:三次握手建立连接、传输数据进行确认、四次挥手断开连接
🤔三次握手建立连接:
①客户端发送请求,服务端接收请求 → 服务端:知道客户端发消息正常
②服务端返回响应,客户端接收到响应 → 客户端:知道服务端收消息和发消息都正常(收到了才会响应嘛)
③客户端发出确认信息,服务端收到信息 → 服务端:知道客户端收到了自己的响应,收消息正常
🤔四次挥手断开连接:
①客户端发送断开连接请求,服务端接收请求 → 服务端:知道客户端已经做好断开连接的准备了
②服务端返回“稍等”响应,客户端收到响应 → 客户端:知道服务端收到了“断开连接”的请求
③服务端将信息处理完毕后,返回给客户端断开响应,客户端接收响应 → 客户端:知道服务端也已经做好断开的准备,并且可能会接收到服务端最后返回的信息
④两边都准备好了,服务端发送确认断开信息 → 两边断开
4. UDP通信
- 无连接、不可靠通信
- 不会事先建立连接 >> 发送端每次把要发送的数据(限制在64KB内),接收端IP等信息封装成一个数据包,发出去
- Java提供
java.net.DatagramSocket类
来实现UDP通信
4.1 一发一收
1) 客户端
① 创建DatagramSocket发送端对象
② 创建DatagramPacket数据包对象,封装发送数据
③ 发送端对象发送数据包的数据
public class UDPClientDemo1 {public static void main(String[] args) throws Exception {System.out.println("========== 客户端启动...");// 目标:完成UDP通信一发一收:客户端开发// 1、创建发送端对象DatagramSocket socket = new DatagramSocket(); // 创建发送端对象,不指定端口号,系统随机分配一个端口号// 2、创建数据包对象,封装要发送的数据byte[] bytes = "你好,我是客户端".getBytes();/*** 参数一:要发送的数据* 参数二:要发送的数据的长度* 参数三:目标主机的IP地址 getByName("127.0.0.1")* 参数四:目标主机的端口号*/DatagramPacket packet = new DatagramPacket(bytes, bytes.length,InetAddress.getLocalHost(), 8080);// 3、让发送端对象发送数据包的数据socket.send(packet);}
}
2) 服务端
① 创建DatagramSocket服务端对象,并注册端口
② 创建DatagramPacket数据包对象,用于接收客户端发来的数据
③ 接收客户端发来的数据
④ 服务端可以获取客户端数据包中的IP地址和端口号
public class UDPServerDemo2 {public static void main(String[] args) throws Exception{System.out.println("============= 服务端启动...");// 目标:完成UDP通信一发一收:服务端开发// 1、创建接收端对象,注册端口DatagramSocket socket = new DatagramSocket(8080);//2、创建数据包对象,用于接受客户端发来的数据byte[] bytes = new byte[1024*64]; // 64KBDatagramPacket packet = new DatagramPacket(bytes, bytes.length);// 3、接收客户端发来的数据,将数据封装到数据包对象的字节数组中socket.receive(packet);// 4、获取数据包对象中的数据,并打印String str = new String(bytes, 0, packet.getLength());System.out.println("服务端收到数据:" + str);// 5、获取对方的IP地址和端口号String ip = packet.getAddress().getHostAddress(); // 获取IP地址int port = packet.getPort();System.out.println("对方IP地址:" + ip);System.out.println("对方端口号:" + port);}
}
⚠️先启动服务端,再启动客户端 → 不可靠传输,客户端发送数据包不会管对方是否接收到
4.2 多发多收
同时收很多客户端的数据,只管接受数据包,不管是哪个客户端
1) 客户端
循环获取需要发送的数据
public class UDPClientDemo1 {public static void main(String[] args) throws Exception {System.out.println("========== 客户端启动...");// 目标:完成UDP通信多发多收:客户端开发// 1、创建发送端对象DatagramSocket socket = new DatagramSocket(); // 创建发送端对象,不指定端口号,系统随机分配一个端口号Scanner scanner = new Scanner(System.in);byte[] bytes = null;// 循环发送数据while (true) {System.out.println("请输入要发送的数据:");// 2、创建数据包对象,封装要发送的数据String msg = scanner.nextLine(); // 键盘录入数据bytes = msg.getBytes(); // 将字符串转换成字节数组// 判断用户输入的指令是否为Exit:是 → 退出循环if ("Exit".equals(msg)) {System.out.println("====客户端退出...");break;}DatagramPacket packet = new DatagramPacket(bytes, bytes.length,InetAddress.getLocalHost(), 8080); // 指定发送的ip地址和端口号// 3、让发送端对象发送数据包的数据socket.send(packet);}}
}
2) 服务端
只需要将接收数据包的代码段用while循环
包起来就可以了
while (true) {// 3、接收客户端发来的数据,将数据封装到数据包对象的字节数组中socket.receive(packet);// 4、获取数据包对象中的数据,并打印String str = new String(bytes, 0, packet.getLength());System.out.println("服务端收到数据:" + str);// 5、获取对方的IP地址和端口号String ip = packet.getAddress().getHostAddress(); // 获取IP地址int port = packet.getPort();System.out.println("对方IP地址:" + ip);System.out.println("对方端口号:" + port);System.out.println("-----------------------------------------");
}
3) 多次运行同一个java类
需要同时运行两个客户端
4) 结果
5. TCP通信
-
面向连接、可靠通信
-
三次握手建立可靠连接,实现端到端的通信
-
Java提供
java.net.Socket类
来实现TCP通信 -
基于管道通信
5.1 一发一收
1)客户端 Socket
public class TCPClientDemo1 {public static void main(String[] args) throws Exception {// 目标:实现TCP通信下一发一收,客户端开发// 1、常见Socket管道对象,请求与服务端的Socket链接,可靠连接Socket socket = new Socket("127.0.0.1", 9999);// 2、从Socket通信管道中得到一个字节输出流对象OutputStream os = socket.getOutputStream();// 3、特殊数据流:接收方接收的顺序和发送方一致DataOutputStream dos = new DataOutputStream(os);dos.writeInt(1);dos.writeUTF("你好,我是客户端");// 按照字节数组的方式发送数据// os.write("你好,我是客户端".getBytes());// 4、关闭流资源socket.close();}
}
- 按照行输出,就要按照行接收,按照字节打印,就要按字节接收
2)服务端 ServerSocket
public class TCPServerDemo2 {public static void main(String[] args) throws Exception {System.out.println("=========服务端启动...");// 目标:实现TCP通信下一发一收,服务端开发// 1、创建一个服务器Socket对象,绑定监听端口ServerSocket ss = new ServerSocket(9999);// 2、调用accept()方法,阻塞等待客户端连接:一旦有客户端连接,会返回一个Socket对象Socket socket = ss.accept();// 3、获取输入流,读取客户端发送的数据InputStream is = socket.getInputStream();// 4、把字节输入流包装为特殊数据流输入流DataInputStream dis = new DataInputStream(is);// 5、读取数据并输出int id = dis.readInt();String msg = dis.readUTF();System.out.println("服务端收到:" + id + " " + msg);// 6、获取客户端信息System.out.println("客户端IP地址:" + socket.getInetAddress().getHostAddress());System.out.println("客户端端口号:" + socket.getPort());System.out.println("-----------------------------------------------------------");}
}
5.2 多发多收(客户端只有一个)
1)客户端
Scanner scanner = new Scanner(System.in);
// 循环发送数据
while (true) {System.out.println("请输入要发送的内容:");String msg = scanner.nextLine();if ("exit".equals(msg)) {System.out.println("====客户端退出...");dos.close();socket.close();break;}dos.writeUTF(msg);dos.flush(); // 刷新缓冲区,将数据写入到Socket缓存区中
}
2)服务端
// 5、读取数据并输出
while(true){String msg = dis.readUTF();System.out.println("服务端收到:" + msg);// 客户端信息:IP地址、端口号System.out.println("客户端IP地址:" + socket.getInetAddress().getHostAddress());System.out.println("客户端端口号:" + socket.getPort());System.out.println("-----------------------------------------");
}
5.3 ⭐同时接收多个客户端的消息
🤔服务端目前只有一个主线程,只能处理一个客户端的通信
- 主线程定义一个循环,负责接收客户端Socket管道连接
- 每接收到一个Socket通信管道后就分配一个独立的线程负责处理它
1)接收数据的线程
public class ServerReader extends Thread{private Socket socket;public ServerReader(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {InputStream is = socket.getInputStream();DataInputStream dis = new DataInputStream(is);// 一直循环读取数据并输出while(true){String msg = dis.readUTF();// 监测客户端退出if("exit".equals(msg)){System.out.println("端口为"+socket.getPort()+"的客户端退出了");System.out.println("-----------------------------------------");break; //客户端退出,接收的管道也关闭,退出循环}System.out.println("服务端收到:" + msg);// 客户端信息:IP地址、端口号System.out.println("客户端IP地址:" + socket.getInetAddress().getHostAddress());System.out.println("客户端端口号:" + socket.getPort());System.out.println("-----------------------------------------");}} catch (Exception e) {e.printStackTrace();}}
}
2)服务端(主线程)
循环接收访问的客户端,然后创建一个ServerReader对象,对管道中的数据进行读取
public class TCPServerDemo2 {public static void main(String[] args) throws Exception {System.out.println("=========服务端启动...");ServerSocket ss = new ServerSocket(9999);while (true) {// 调用accept()方法,阻塞等待客户端连接:一旦有客户端连接,会返回一个Socket对象Socket socket = ss.accept();new ServerReader(socket).start();}}
}
3)客户端
public class TCPClientDemo1 {public static void main(String[] args) throws Exception {System.out.println("============客户端启动...");Socket socket = new Socket("127.0.0.1", 9999);// 从Socket通信管道中得到一个字节输出流对象OutputStream os = socket.getOutputStream();DataOutputStream dos = new DataOutputStream(os);Scanner scanner = new Scanner(System.in);// 循环发送数据while (true) {System.out.println("请输入要发送的内容:");String msg = scanner.nextLine();dos.writeUTF(msg);if ("exit".equals(msg)) {System.out.println("====客户端退出...");dos.close();socket.close();break;}dos.flush(); // 刷新缓冲区,将数据写入到Socket缓存区中}}
}
5.4 B/S架构的原理(网站开发)
1)服务端
服务器必须给浏览器响应HTTP协议规定的数据格式
public class ServerReader extends Thread {private Socket socket;public ServerReader(Socket socket) {this.socket = socket;}@Overridepublic void run() {try {// 给当前对象的浏览器管道响应网页数据OutputStream os = socket.getOutputStream();// 使用打印流:方便自动换行PrintWriter pw = new PrintWriter(os);// 打印网页数据pw.println("HTTP/1.1 200 OK");pw.println("Content-Type:text/html;charset=utf-8");pw.println(""); // 空行pw.println("<html>");pw.println("<head>");pw.println("<title>");pw.println("测试");pw.println("</title>");pw.println("</head>");pw.println("<body>");pw.println("<h1 style = 'color:red'>你好,我是服务器</h1>");pw.println("</body>");pw.println("</html>");pw.close();socket.close();} catch (Exception e) {e.printStackTrace();System.out.println("服务器异常!");}}
}
public class ServerDemo {public static void main(String[] args) throws Exception {//目标:学习B/S架构的服务器端程序System.out.println("=========服务器端启动...");// 创建服务器对象,并注册端口ServerSocket ss = new ServerSocket(8080);while (true) {System.out.println("服务器端在8080端口监听...");// 调用accept()方法,阻塞等待客户端连接:一旦有客户端连接,会返回一个Socket对象Socket socket = ss.accept();System.out.println("一个客户端连接了..."+socket.getInetAddress()+":"+socket.getPort());new ServerReader(socket).start();}}
}
⚠️这样会创建很多线程
2)使用线程池优化
public class ServerDemo {public static void main(String[] args) throws Exception {//目标:学习B/S架构的服务器端程序System.out.println("=========服务器端启动...");// 创建服务器对象,并注册端口ServerSocket ss = new ServerSocket(8080);// 创建一个线程池,用来存储线程对象ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 10, 10,TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100),Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());while (true) {System.out.println("服务器端在8080端口监听...");// 调用accept()方法,阻塞等待客户端连接:一旦有客户端连接,会返回一个Socket对象Socket socket = ss.accept();System.out.println("一个客户端连接了..."+socket.getInetAddress()+":"+socket.getPort());// 创建一个线程对象,完成数据的读取工作 pool.execute(new ServerReader(socket));}}
}
💡这里直接将继承了Thread的ServerReader对象作为参数传给了pool,而不是使用Runnable的实现类对象,是因为Thread本身就是Runnable的实现类对象
,如果要将ServerReader改写成Runnable的实现类对象也是可以的