概述
网络编程可以让程序与网络上的其他设备中的程序进行数据交互
常见的通信模式有:Client-Server(C/S)、Browser-Server(B/S)
实现网络通信的三要素
- IP地址:设备在网络中的地址,是唯一的标识
- 端口:应用程序在设备中唯一标识
- 协议:数据在网络中的传输规则,常见的协议有UDP协议和TCP协议
IP用于定位网络上的设备,有IPv4,IPv6
IP相关命令
- ipconfig 查看本机IP地址
- ping IP地址 查看网络地址是否连通
IP地址操作类InetAddress
名称 | 说明 |
---|---|
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地址对应的主机,连通返回true |
package com.spark.study;
import java.net.InetAddress;
/**
* IpDemo class
* description: InetAddress类的基本使用
*
* @author Administrator
* @date 2023/5/22
*/
public class IpDemo {
public static void main(String[] args) throws Exception {
// 获取本机地址对象
InetAddress ip = InetAddress.getLocalHost();
// 获取主机名称
System.out.println(ip.getHostName());
// 获取IP地址
System.out.println(ip.getHostAddress());
// 获取域名对象
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
// 获取主机名称
System.out.println(ip2.getHostName());
// 获取ip地址
System.out.println(ip2.getHostAddress());
// 获取公网IP对象
InetAddress ip3 = InetAddress.getByName("110.242.68.4");
System.out.println(ip3.getHostName());
System.out.println(ip3.getHostAddress());
// 判断是否能通
System.out.println(ip2.isReachable(5000));
}
}
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对象并指定端口号 |
方法 | 说明 |
---|---|
public void send(DatagramPacket dp) | 发送数据包 |
public void receive(DatagramPacket p) | 接收数据包 |
UDP通信实现一发一收
package com.spark.udpstudy;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* ClientDemo class
* description: 发送方
*
* @author Administrator
* @date 2023/5/24
*/
public class ClientDemo {
public static void main(String[] args) throws Exception {
// 创建发送方对象,默认会分配端口号
DatagramSocket client = new DatagramSocket();
System.out.println("客户端启动了~~~~~~~~~~~~~~~~");
// 要发送的数据
String data = "我是要给服务端发送的数据";
byte [] buffer = data.getBytes();
// 创建数据包对象
DatagramPacket packet = new DatagramPacket(buffer,buffer.length, InetAddress.getLocalHost(),8888);
client.send(packet);
// 关闭服务
client.close();
}
}
package com.spark.udpstudy;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* ServerDemo class
* description: 接收方
*
* @author Administrator
* @date 2023/5/24
*/
public class ServerDemo {
public static void main(String[] args) throws Exception {
// 创建接收方对象
DatagramSocket server = new DatagramSocket(8888);
System.out.println("服务端启动了~~~~");
// 接收数据
byte [] buffer = new byte[1024*64];
// 创建数据包对象
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
server.receive(packet);
// 取出数据
byte[] data= packet.getData();
System.out.println("服务端取到数据:"+new String(data,0,packet.getLength()));
System.out.println("发送方IP地址:"+packet.getSocketAddress());
System.out.println("发送方port端口:"+packet.getPort());
// 关闭服务
server.close();
}
}
UDP通信实现多发多收
package com.spark.udpstudy;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
/**
* ClientDemo2 class
* description: 多发多收 客户端
*
* @author Administrator
* @date 2023/5/29
*/
public class ClientDemo2 {
public static void main(String[] args) throws Exception {
// 创建发送方对象
DatagramSocket socket = new DatagramSocket(6666);
System.out.println("客户端启动了~~~~~~~~~~~~~~~~");
// 接收键盘输入数据
Scanner scanner = new Scanner(System.in);
// 死循环 用于多发
while(true){
System.out.println("请说:");
String msg = scanner.nextLine();
if("exit".equals(msg)){
System.out.println("离线成功!");
socket.close();
break;
}
byte [] buffer = msg.getBytes();
// 创建数据包对象
DatagramPacket packet = new DatagramPacket(buffer,buffer.length, InetAddress.getLocalHost(),8888);
socket.send(packet);
}
}
}
package com.spark.udpstudy;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* ServerDemo2 class
* description: 多发多收 服务端
*
* @author Administrator
* @date 2023/5/29
*/
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
// 创建接收方对象
DatagramSocket socket = new DatagramSocket(8888);
System.out.println("服务端启动了~~~~~~~~~~~~~~~~");
// 接收数据
byte [] data = new byte[1024*64];
// 死循环 用于多收
while(true){
// 创建数据包对象接收数据
DatagramPacket packet = new DatagramPacket(data,data.length);
socket.receive(packet);
// 取出数据
System.out.println("接收到了来自"+packet.getSocketAddress()+"的消息:"+new String(packet.getData(),0,packet.getLength()));
}
}
}
广播通信:当前主机与所在网络中的所有主机通信
UDP实现广播通信
-
发送端发送的数据包的目的地使用广播地址、指定端口 (255.255.255.255 , 9999)
-
本机所在网段的其他主机的程序只要匹配端口成功就可以收到消息了 (9999)
package com.spark.updbord;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
/**
* ClientDemo class
* description: 广播通信 客户端
*
* @author Administrator
* @date 2023/5/29
*/
public class ClientDemo {
public static void main(String[] args) throws Exception {
// 创建发送端对象
DatagramSocket socket = new DatagramSocket();
System.out.println("客户端启动了");
String msg = "这是一条广播消息";
byte [] buffer = msg.getBytes(StandardCharsets.UTF_8);
// 创建数据包
DatagramPacket packet = new DatagramPacket(buffer,buffer.length, InetAddress.getByName("255.255.255.255"),9999);
socket.send(packet);
socket.close();
}
}
package com.spark.updbord;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* ServerDemo class
* description: 广播通信 服务端
*
* @author Administrator
* @date 2023/5/29
*/
public class ServerDemo {
public static void main(String[] args) throws Exception {
// 创建服务端对象
DatagramSocket server = new DatagramSocket(9999);
byte [] data = new byte[1024*64];
// 创建数据包
DatagramPacket packet = new DatagramPacket(data,data.length);
server.receive(packet);
System.out.println("接收到了来自"+packet.getSocketAddress()+"的消息:"+new String(packet.getData(),0,packet.getLength()));
server.close();
}
}
组播通信:当前主机与选定的一组主机的通信
UDP实现组播通信:
-
发送端的数据包的目的地IP地址是组播IP (例如:224.0.0.1 , 9999)
-
接收端必须绑定该组播(224.0.0.1)端口还要对应发送端的目的地端口9999,这样即可接收该组播消息
-
DatagramSocket的子类MulticastSocket可以在接收端绑定组播
package com.spark.udpmulticast;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
/**
* ClientDemo class
* description: 组播通信 客户端
*
* @author Administrator
* @date 2023/5/29
*/
public class ClientDemo {
public static void main(String[] args) throws Exception {
// 创建发送端对象
DatagramSocket socket = new DatagramSocket();
System.out.println("客户端启动了");
String msg = "这是一条组播消息";
byte [] buffer = msg.getBytes(StandardCharsets.UTF_8);
// 创建数据包
DatagramPacket packet = new DatagramPacket(buffer,buffer.length, InetAddress.getByName("224.0.0.1"),9999);
socket.send(packet);
socket.close();
}
}
package com.spark.udpmulticast;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
/**
* ServerDemo class
* description: 组播通信 服务端
*
* @author Administrator
* @date 2023/5/29
*/
public class ServerDemo {
public static void main(String[] args) throws Exception {
// 创建服务端对象
MulticastSocket server = new MulticastSocket(9999);
// 绑定组播ip
server.joinGroup(InetAddress.getByName("224.0.0.1"));
byte [] data = new byte[1024*64];
// 创建数据包
DatagramPacket packet = new DatagramPacket(data,data.length);
server.receive(packet);
System.out.println("接收到了来自"+packet.getSocketAddress()+"的消息:"+new String(packet.getData(),0,packet.getLength()));
server.close();
}
}
TCP通信
TCP是一种面向连接、安全、可靠的传输数据的协议。
传输前采用 “三次握手” 方式,点对点通信,是可靠的
在连接中可进行大数据量的传输
快速入门
客户端与服务端进行通信时,发送数据使用字节输出流,接收数据使用字节输入流
在Java中只要是使用java.net.Socket类实现通信,底层即是使用了TCP协议
客户端通信对象为Socket,服务端通信对象为ServerSocket
TCP通信实现一发一收
package com.spark.tcpstudy;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
/**
* ClientDemo class
* description: TCP通信实现一发一收 客户端
*
* @author Administrator
* @date 2023/5/31
*/
public class ClientDemo {
public static void main(String[] args) throws Exception{
// 创建Socket对象建立与服务端的连接
Socket socket = new Socket("127.0.0.1",8888);
System.out.println("客户端启动了~~~~~~~~~~~~~~~~");
// 获取输出流
OutputStream os = socket.getOutputStream();
// 包装低级流为打印流
PrintStream ps = new PrintStream(os);
ps.println("客户端发送了一条数据");
ps.flush();
}
}
package com.spark.tcpstudy;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* ServerDemo class
* description: TCP通信实现一发一收 服务端
*
* @author Administrator
* @date 2023/5/31
*/
public class ServerDemo {
public static void main(String[] args) throws Exception {
// 创建ServerSocket对象
ServerSocket server = new ServerSocket(8888);
System.out.println("服务端启动了~~~~~~~~~~~~~~~~");
// 接收客户端Socket对象
Socket client = server.accept();
// 获取输入流,接收客户端传递的数据
InputStream is = client.getInputStream();
// 将低级流包装成缓冲字符流
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
// 读取数据
String msg;
if(null!=(msg = reader.readLine())){
System.out.println(msg);
}
}
}
TCP通信实现多发多收
package com.spark.tcpstudy;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* ClientDemo2 class
* description: TCP通信实现多发多收 客户端
*
* @author Administrator
* @date 2023/6/3
*/
public class ClientDemo2 {
public static void main(String[] args) throws Exception {
// 创建Socket对象
Socket socket = new Socket("127.0.0.1",8888);
System.out.println("客户端启动成功~~~~~~~~~~~~~~~~");
OutputStream os = socket.getOutputStream();
// 将低级流包装为打印流
PrintStream ps = new PrintStream(os);
// 从键盘接收消息
Scanner scanner = new Scanner(System.in);
while(true){
System.out.println("请说:");
String msg = scanner.nextLine();
ps.println(msg);
ps.flush();
if("exit".equals(msg)){
socket.close();
break;
}
}
}
}
package com.spark.tcpstudy;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* ServerDemo2 class
* description: TCP通信实现多发多收 服务端
*
* @author Administrator
* @date 2023/6/3
*/
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
// 创建ServerSocket对象
ServerSocket server = new ServerSocket(8888);
System.out.println("服务端启动成功~~~~~~~~~~~~~~~~");
Socket socket = server.accept();
// 获取输入流
InputStream is = socket.getInputStream();
// 将低级流包装成缓冲字符流
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String msg;
while(null!=(msg=reader.readLine())){
System.out.println("接收到了来自"+socket.getInetAddress().getHostAddress()+"的消息:"+msg);
}
}
}
是否可以同时接收多个客户端的消息?
不可以,现在服务端只有一个线程,只能与一个客户端进行通信。
处理多个客户端通信请求
引入多线程,改写服务端可以处理多个客户端通信请求
package com.spark.tcpstudy;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* ReaderThread class
* description: 读取数据线程
*
* @author Administrator
* @date 2023/6/3
*/
public class ReaderThread extends Thread {
private Socket socket;
public ReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try{
// 获取输入流
InputStream is = socket.getInputStream();
// 将低级流包装成缓冲字符流
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String msg;
while(null!=(msg=reader.readLine())){
System.out.println("接收到了来自"+socket.getRemoteSocketAddress()+"的消息:"+msg);
}
}catch(Exception e){
System.out.println(socket.getRemoteSocketAddress()+"下线了~~~~~~~~");
}
}
}
package com.spark.tcpstudy;
import java.net.ServerSocket;
import java.net.Socket;
/**
* ServerDemo2 class
* description: TCP通信实现多发多收 服务端
*
* @author Administrator
* @date 2023/6/3
*/
public class ServerDemo3 {
public static void main(String[] args) throws Exception {
// 创建ServerSocket对象
ServerSocket server = new ServerSocket(8888);
System.out.println("服务端启动成功~~~~~~~~~~~~~~~~");
while(true){
Socket socket = server.accept();
new ReaderThread(socket).start();
System.out.println(socket.getRemoteSocketAddress()+"上线了~~~~~~~~~~~~~~~~");
}
}
}
客户端代码不进行修改,但是启动多个客户端时如果遇到该弹框,需要设置该程序启动允许多启动
目前的通信架构存在的问题
-
客户端与服务端的线程模型是:N-N的关系
-
每启动一个客户端就要创建一个线程,当客户端数量庞大时,线程占用资源越大,系统瘫痪越快
引入线程池处理多个客户端消息
package com.spark.tcpthreadpool;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
/**
* ClientDemo2 class
* description: 引入线程池优化通信 客户端
*
* @author Administrator
* @date 2023/6/3
*/
public class ClientDemo {
public static void main(String[] args) throws Exception {
// 创建Socket对象
Socket socket = new Socket("127.0.0.1",8888);
System.out.println("客户端启动成功~~~~~~~~~~~~~~~~");
OutputStream os = socket.getOutputStream();
// 将低级流包装为打印流
PrintStream ps = new PrintStream(os);
// 从键盘接收消息
Scanner scanner = new Scanner(System.in);
while(true){
System.out.println("请说:");
String msg = scanner.nextLine();
ps.println(msg);
ps.flush();
}
}
}
package com.spark.tcpthreadpool;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Socket;
/**
* ServerRunnableThread class
* description: 线程任务
*
* @author Administrator
* @date 2023/6/3
*/
public class ServerRunnableThread implements Runnable {
private Socket socket;
public ServerRunnableThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try{
// 获取输入流
InputStream is = socket.getInputStream();
// 将低级流包装成缓冲字符流
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String msg;
while(null!=(msg=reader.readLine())){
System.out.println("接收到了来自"+socket.getRemoteSocketAddress()+"的消息:"+msg);
}
}catch(Exception e){
System.out.println(socket.getRemoteSocketAddress()+"下线了~~~~~~~~");
}
}
}
package com.spark.tcpthreadpool;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
/**
* ServerDemo2 class
* description: 引入线程池优化通信 服务端
*
* @author Administrator
* @date 2023/6/3
*/
public class ServerDemo {
public static ExecutorService pool = new ThreadPoolExecutor(3,
5,
5,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) throws Exception {
// 创建ServerSocket对象
ServerSocket server = new ServerSocket(8888);
System.out.println("服务端启动成功~~~~~~~~~~~~~~~~");
while(true){
Socket socket = server.accept();
pool.execute(new ServerRunnableThread(socket));
System.out.println(socket.getRemoteSocketAddress()+"上线了~~~~~~~~~~~~~~~~");
}
}
}
使用线程池的优势:
-
服务器可以复用线程处理多个客户端,可以避免系统瘫痪
-
适合客户端通信时长较短的场景
实战案例
① TCP通信实现即时通信
即时通信是指一个客户端的消息发出去,其他客户端可以接收到。之前消息都是发送给服务端,即时通信需要进行端口转发的设计思想。
package com.spark.tcpexample;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* ClientDemo class
* description: TCP通信实战 即时通信客户端
*
* @author Administrator
* @date 2023/6/3
*/
public class ClientDemo {
public static void main(String[] args) throws Exception {
// 创建Socket对象
Socket client = new Socket("127.0.0.1",8888);
System.out.println("客户端启动了~~~~~~~~~~~~~~~~");
// 启动线程用于读服务端发送来的数据
new ClientReadThread(client).start();
// 获取输出流用于写数据
OutputStream os = client.getOutputStream();
// 将低级流包装成打印流
PrintStream ps = new PrintStream(os);
// 发送数据
Scanner scanner = new Scanner(System.in);
while(true){
System.out.println("请说:");
String msg = scanner.nextLine();
ps.println(msg);
ps.flush();
}
}
}
class ClientReadThread extends Thread {
private Socket socket;
public ClientReadThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try{
// 获取输入流读取数据
InputStream is = socket.getInputStream();
// 将低级流包装为缓冲字符流
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = reader.readLine()) != null){
System.out.println("收到消息:"+msg);
}
}catch (Exception e){
System.out.println("被服务器踢出去了...");
}
}
}
package com.spark.tcpexample;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* ServerDemo class
* description: TCP通信实战 即时通信服务端
*
* @author Administrator
* @date 2023/6/3
*/
public class ServerDemo {
// 创建列表保存在线的客户端Socket对象
public static List<Socket> onlines = new ArrayList<>();
// 初始化线程池
public static ExecutorService pool = new ThreadPoolExecutor(
3,
5,
6,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
public static void main(String[] args) throws Exception {
// 创建ServerSocket对象
ServerSocket server = new ServerSocket(8888);
System.out.println("服务端启动了~~~~~~~~~~~~~~~~");
// 获取Socket对象
while(true){
Socket client = server.accept();
pool.execute(new ServerReadThread(client));
System.out.println(client.getRemoteSocketAddress()+"上线了...");
// 将socket对象加入到列表中
onlines.add(client);
}
}
}
class ServerReadThread implements Runnable {
private Socket socket;
public ServerReadThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try{
// 获取输入流
InputStream is = socket.getInputStream();
// 将低级流包装成缓冲字符流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while(null!=(msg = br.readLine())){
System.out.println("收到了来自"+socket.getRemoteSocketAddress() + "的消息:"+msg);
// 将数据转发到所有的客户端
sendMessageToAll(msg);
}
}catch(Exception e){
System.out.println(socket.getRemoteSocketAddress() + "下线了...");
ServerDemo.onlines.remove(socket);
}
}
// 转发消息
private void sendMessageToAll(String msg) throws Exception{
// 遍历所有在线客户端Socket集合
for (Socket socket : ServerDemo.onlines) {
// 获取输出流
PrintStream ps = new PrintStream(socket.getOutputStream());
ps.println(msg);
ps.flush();
}
}
}
② TCP通信模拟B/S系统
B/S系统我们不再开发客户端,只需要提供服务端,使用浏览器发送请求即可。
注意:服务器必须给浏览器响应HTTP协议格式的数据,否则浏览器不识别
package com.spark.tcpexample;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
/**
* BsServerDemo class
* description: TCP通信实战 模拟BS架构
*
* @author Administrator
* @date 2023/6/3
*/
public class BsServerDemo {
// 定义线程池
public static ExecutorService pool = new ThreadPoolExecutor(
3,
5,
6,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
public static void main(String[] args) throws Exception {
// 创建ServerSocket对象
ServerSocket server = new ServerSocket(8888);
// 获取Socket对象
Socket socket = server.accept();
// 执行线程任务
pool.execute(new BsServerRunnable(socket));
}
}
class BsServerRunnable implements Runnable{
private Socket socket;
public BsServerRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try{
// 获取输出流
PrintStream ps = new PrintStream(socket.getOutputStream());
// 定义协议版本 状态
// 必须响应HTTP协议格式数据 否则浏览器不识别
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:35px'>服务端响应数据给浏览器了</div>");
ps.flush();
}catch(Exception e){
e.printStackTrace();
}
}
}