网络通信三要素
IP地址: 设备在网络中的地址,是唯一的标识
端口: 应用程序在设备中的唯一标识
协议: 数据在网络中传输的规则,常见的协议有UDP协议和TCP协议
IP地址
常见的IP分类有IPv4和IPv6
IPv6: 128位(16个字节)
IPv6分成8个整数,每个整数用四个十六进制位表示,数之间用冒号( : )分开
IP地址形式:
- 公网地址和私有地址(局域网使用)
- 192.168.开头的就是常见的局域网地址, 范围即为192.168.0.0–192.168.255.255, 专门为组织机构内部使用
IP常用命令:
- ipconfig: 查看本机IP地址
- ping IP地址: 检查网络是否连通
本机IP: 127.0.0.1或者localhost, 称为回送地址也可称本地回环地址, 只会寻找当前所在本机
IP地址操作类-InetAddress
- 此类表示Internet协议(IP)地址
InetAddress API如下
名称 | 说明 |
---|---|
public statis 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 |
public static void main(String[] args) throws IOException {
//获取本机地址对象
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost.getHostName());
System.out.println(localHost.getHostAddress());
//获取域名ip对象
InetAddress ip = InetAddress.getByName("www.baidu.com");
System.out.println(ip.getHostName());
System.out.println(ip.getHostAddress());
//获取公网IP对象
InetAddress ip1 = InetAddress.getByName("39.156.66.18");
System.out.println(ip1.getHostName());
System.out.println(ip1.getHostAddress());
//判断是否能通:ping 5s之内测试是否可通
System.out.println(ip1.isReachable(5000));
}
端口号
端口号: 标识正在计算机设备上运行的进程(程序), 被规定为一个16位的二进制,范围是0 ~ 65535
端口类型
- 周知端口: 0 ~ 1023, 被预先定义的知名应用占用(如: HTTP占用80, FTP占用21 )
- 注册端口: 1024 ~ 49151, 分配给用户进程或某些应用程序(如: Tomcat占用8080, MySQL占用3306)
- 动态端口: 49152 ~ 65535, 之所以称为动态端口, 是因为它一般不固定分配某种进程, 而是动态分配
协议
连接和通信数据的规则被称为网络通信协议
TCP协议
特点
- 必须双方先建立连接,它是一种面向连接的可靠通信协议
- 传输前,采用"三次握手"方式建立连接,所以是可靠
- 在连接中可进行大数据变量的传输
- 连接, 发送数据都需要确认, 且传输完毕后, 还需释放已建立的连接, 通信效率极低
通信场景
- 对信息安全要求较高的场景,例如: 文件下载, 金融等数据通信
TCP三次握手建立连接
TCP四次挥手断开连接
UDP协议
特点
- UDP是一种无连接, 不可靠传输的协议
- 将数据源IP, 目的地IP和端口封装成数据包, 不需要建立连接
- 每个数据包的大小限制在64KB内
- 发送不管对方是否准备好, 接收方收到也不确认
- 可以广播发送, 发送数据结束时无需释放资源, 开销小, 速度快
通信场景
- 语音通话, 视频会议等
UDP通信
DatagramPacket: 数据包对象
构造器 | 说明 |
---|---|
public DatagramPacket(byte[] buf, int length, InetAddress address, int port) | 创建发送端数据包对象 |
public DatagramPacket(byte[] buf, int length) | 创建接收端的数据包对象 |
发送端:
buf : 要发送的内容, 字节数组
length : 要发送的内容的字节长度
address : 接收端的IP地址对象
port : 接收端的端口号
接收端:
buf : 用来存储接收的内容
length : 能够接收内容的长度
DatagramSocket: 数据包对象
构造器 | 说明 |
---|---|
public DatagramSocket() | 创建发送端的Socket对象, 系统会随机分配一个端口号 |
public DatagramSocket(int port) | 创建接收端的Socket对象并指定端口号 |
DatagramSocket类成员方法
方法 | 说明 |
---|---|
public void send(DatagramPacket dp) | 发送数据包 |
public void receive(DatagramPacket p) | 接收数据包 |
接收端
public class Server {
public static void main(String[] args) throws IOException {
//创建接收端对象
DatagramSocket socket = new DatagramSocket(8888);
//创建一个数据包对象接收数据
byte[] buffer = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
//等待接收数据
socket.receive(packet);
//取出数据
int length = packet.getLength();
String rs = new String(buffer,0,length);
System.out.println("收到了:" + rs);
socket.close();
}
}
发送端
public class ClientDemo {
public static void main(String[] args) throws IOException {
//创建发送端对象:发送端自带默认端口号
DatagramSocket socket = new DatagramSocket();
//创建数据包对象封装数据
byte[] buffer = "Hello~".getBytes();
DatagramPacket packet = new DatagramPacket(buffer,buffer.length, InetAddress.getLocalHost(),8888);
//发送数据
socket.send(packet);
socket.close();
}
}
TCP通信
一发一收
- 在java中只要是使用java.net.Socket类实现通信, 底层即是使用了TCP协议
接收端
public class ServerDemo {
public static void main(String[] args) throws IOException {
//注册端口
ServerSocket serverSocket = new ServerSocket(7777);
//等待接收客户端的连接请求
Socket socket = serverSocket.accept();
//从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
//把字节输入流包装成缓冲字符输入流接收消息
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
if ((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress()+ "-----" + msg);
}
}
}
发送端
public class ClientDemo {
public static void main(String[] args) throws IOException {
/**
* 创建Socket通信管道请求有服务端的连接
* 参数一:服务端的IP地址
* 参数二:服务端的端口
*/
Socket socket = new Socket("127.0.0.1",7777);
//从socket通信管道中得到一个字节输出流,负责发送数据
OutputStream os = socket.getOutputStream();
//把字节流包装成打印流
PrintStream ps = new PrintStream(os);
//发送消息
ps.println("我是TCP的客户端");
ps.flush();
}
}
多发多收
- 可以使用死循环控制服务端收完消息继续等待接收下一个消息
- 客户端也可以使用死循环等待用户不断输入消息
- 客户端一旦输入了exit, 则关闭客户端程序并释放资源
服务端
public class ServerDemo {
public static void main(String[] args) throws IOException {
//注册端口
ServerSocket serverSocket = new ServerSocket(7777);
//等待接收客户端的连接请求
Socket socket = serverSocket.accept();
//从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
//把字节输入流包装成缓冲字符输入流接收消息
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null){
System.out.println(socket.getRemoteSocketAddress()+ "-----" + msg);
}
}
}
客户端
public class ClientDemo {
public static void main(String[] args) throws IOException {
/**
* 创建Socket通信管道请求有服务端的连接
* 参数一:服务端的IP地址
* 参数二:服务端的端口
*/
Socket socket = new Socket("127.0.0.1",7777);
//从socket通信管道中得到一个字节输出流,负责发送数据
OutputStream os = socket.getOutputStream();
//把字节流包装成打印流
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true){
System.out.println("请说:");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
}
}
本案例实现了多发多收,但是不可以同时接收多个客户端的纤细, 因为服务端现在只有一个线程, 只能与一个客户端进行通信
同时接收多个客户端消息
- 主线程定义了循环负责接收客户端Socket管道连接
- 没接受到一个Socket通信管道后分配一个独立的线程负责处理它
客户端同上
服务端
public class ServerDemo {
public static void main(String[] args) throws IOException {
//注册端口
ServerSocket serverSocket = new ServerSocket(7777);
while (true){
//每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取信息
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
//开始创建独立线程处理socket
new ServerReaderThread(socket).start();
}
}
}
独立线程
public class ServerReaderThread extends Thread {
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try{
//从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
//把字节输入流包装成缓冲字符输入流接收消息
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + "-----" + msg);
}
} catch (Exception e){
System.out.println(socket.getRemoteSocketAddress() + "下线了");
}
}
}
- 先启动服务端
- 然后启动客户端一
- 然后启动客户端二
- 客户端一发送消息
- 客户端二发送消息
- 关闭客户端一
- 关闭客户端二
使用线程池优化
客户端同上
创建Runnable任务交给线程池处理
public class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try{
//从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
//把字节输入流包装成缓冲字符输入流接收消息
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + "-----" + msg);
}
} catch (Exception e){
System.out.println(socket.getRemoteSocketAddress() + "下线了");
}
}
}
服务端
public class ServerDemo {
//线程池对象
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 IOException {
//注册端口
ServerSocket serverSocket = new ServerSocket(7777);
while (true){
//每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取信息
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress() + "上线了");
pool.execute(new ServerReaderRunnable(socket));
}
}
}
- 服务端可以复用线程处理多个客户端, 可以避免系统瘫痪
- 适合客户端通信时常较短的场景