一、网络通信三要素
- IP地址:设备在网络中的地址,是唯一的标识
- 端口号:应用程序在设备中唯一的标识
- 协议:数据在网络中传输的规则,常见的协议有UDP协议和TCP协议
1 IP地址
- IP(Internet Protocol):全称”互联网协议地址”,是分配给上网设备的唯一标志。
- 常见分类:IPv4和IPv6
IP地址形式:
- 公网地址和私有地址(局域网使用)
- 192.168.开头就是常见的局域网地址,范围为192.168.0.0~192.168.255.255,专门为组织机构内部使用
IP常用命令:
- ipconfig:查看本机IP地址
- ping IP地址:检查网络是否连通
特殊IP地址:
- 本机IP:127.0.0.1 或者 localhost(称为回送地址也可称为本地回环地址,只会寻找当前所在本机)
2 IP地址操作类 InetAddress
/**
目标:InetAddress类概述(了解)
一个该类的对象就代表一个IP地址对象。
InetAddress类成员方法:
static InetAddress getLocalHost()
* 获得本地主机IP地址对象。
static InetAddress getByName(String host)
* 根据IP地址字符串或主机名获得对应的IP地址对象。
String getHostName()
* 获得主机名。
String getHostAddress()
* 获得IP地址字符串。
*/
public class InetAddressDemo01 {
public static void main(String[] args) throws Exception {
// 1.获取本机地址对象。
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));
}
}
2 端口号
- 端口号:标识正在计算机设备上运行的进程(程序),被规定为一个16位的二进制,范围是0~65535。
- 端口类型
注意:同一设备中不能出现两个程序的端口号一样,否则出错
3 协议
网络通信协议有两套参考模型:
- OSI参考模型:世界互联协议标准,全球通信规范,由于此模型过于理想化,未能在因特网上进行广泛推广。
- TCP/IP参考模型(或TCP/IP协议):事实上的国际标准。
传输层的2个常见协议:
- TCP(Transmission Control Protocol) :传输控制协议
- UDP(User Datagram Protocol):用户数据报协议
TCP协议特点:
- 使用前双方必须建立连接,是一种面向连接的可靠通信协议。
- 传输前采用“三次握手”方式建立连接,所以可靠。
- 在连接中可进行大数据量的传输。
- 连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率较低。
应用场景:
对信息安全要求较高的场景:文件下载、金融等数据通信。
三次握手:
四次挥手:
UDP协议:
- 无连接、不可靠传输的协议。
- 将数据源IP、目的地IP和端口封装成数据包,不需要建立连接。
- 每个数据包大小限制在64KB内。
- 发送不管对方是否准备好,接收方收到也不确认,所以不可靠。
- 可以广播发送,发送数据结束时无需释放资源,开销小、速度快。
常用场景:
- 语音通话、视频会话等。
二、UDP通信
1 DatagramPacket:数据包对象
2 DatagramSocket:发送端和接收端对象
3 使用UDP通信实现:发送消息、接收消息
/**
发送端 一发 一收
*/
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
System.out.println("=====客户端启动======");
// 1、创建发送端对象:发送端自带默认的端口号(人)
DatagramSocket socket = new DatagramSocket(6666);
// 2、创建一个数据包对象封装数据(韭菜盘子)
/**
public DatagramPacket(byte buf[], int length,
InetAddress address, int port)
参数一:封装要发送的数据(韭菜)
参数二:发送数据的大小
参数三:服务端的主机IP地址
参数四:服务端的端口
*/
byte[] buffer = "我是一颗快乐的韭菜,你愿意吃吗?".getBytes();
DatagramPacket packet = new DatagramPacket( buffer, buffer.length,
InetAddress.getLocalHost() , 8888);
// 3、发送数据出去
socket.send(packet);
socket.close();
}
}
/**
发送端 一发 一收
*/
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
System.out.println("=====客户端启动======");
// 1、创建发送端对象:发送端自带默认的端口号(人)
DatagramSocket socket = new DatagramSocket(6666);
// 2、创建一个数据包对象封装数据(韭菜盘子)
/**
public DatagramPacket(byte buf[], int length,
InetAddress address, int port)
参数一:封装要发送的数据(韭菜)
参数二:发送数据的大小
参数三:服务端的主机IP地址
参数四:服务端的端口
*/
byte[] buffer = "我是一颗快乐的韭菜,你愿意吃吗?".getBytes();
DatagramPacket packet = new DatagramPacket( buffer, buffer.length,
InetAddress.getLocalHost() , 8888);
// 3、发送数据出去
socket.send(packet);
socket.close();
}
}
4 多发多收
需求:使用UDP通信方式开发接收端和发动端
分析:
- 发送端可以一直发送消息
- 接收端可以不断的结束多个发送端的消息展示
- 发送端输入了exit则结束发送端程序
/**
发送端 多发 多收
*/
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
System.out.println("=====客户端启动======");
// 1、创建发送端对象:发送端自带默认的端口号(人)
DatagramSocket socket = new DatagramSocket(7777);
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);
}
}
}
/**
接收端
*/
public class ServerDemo2 {
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);
while (true) {
// 3、等待接收数据。
socket.receive(packet);
// 4、取出数据即可
// 读取多少倒出多少
int len = packet.getLength();
String rs = new String(buffer,0, len);
System.out.println("收到了来自:" + packet.getAddress() +", 对方端口是" + packet.getPort() +"的消息:" + rs);
}
}
}
- UDP的接收端为什么可以接收很多发送端的消息?
- 答:接收端只负责接收数据包,无所谓是哪个发送端的数据包
5 广播、组播
5.1 UDP的三种通信方式
- 单播:单台主机与单台主机之间的通信
- 广播:当前主机与所在网络中的所有主机通信
- 组播:当前主机与选定的一组主机的通信
5.2 UDP如何是实现组播
- 使用组播地址:224.0.0.0~239.225.225.225
- 具体操作:
- 发送端的数据包的目的地是组播IP(如:224.0.1.1 端口:9999)
- 接收端必须绑定组播IP(224.0.1.1),端口还要注册发送端的目的端口9999,这样即可接受该组播消息
- DatagramSocket的子类MulticastSocket可以在接收端绑定组播IP
- 这个不太重要,要看代码可去项目里自己看
三、TCP通信
1 TCP通信模式演示:
2 基本消息传输
客户端
客户端发送消息:
服务端
服务端接收消息:
代码展示:
/**
目标:完成Socket网络编程入门案例的客户端开发,实现1发1收。
*/
public class ClientDemo1 {
public static void main(String[] args) {
try {
System.out.println("====客户端启动===");
// 1、创建Socket通信管道请求有服务端的连接
// public Socket(String host, int port)
// 参数一:服务端的IP地址
// 参数二:服务端的端口
Socket socket = new Socket("127.0.0.1", 7777);
// 2、从socket通信管道中得到一个字节输出流 负责发送数据
OutputStream os = socket.getOutputStream();
// 3、把低级的字节流包装成打印流
PrintStream ps = new PrintStream(os);
// 4、发送消息
ps.println("我是TCP的客户端,我已经与你对接,并发出邀请:约吗?");
ps.flush();
// 关闭资源。
// socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
目标:开发Socket网络编程入门代码的服务端,实现接收消息
*/
public class ServerDemo2 {
public static void main(String[] args) {
try {
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);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3 TCP实现多发多收消息
要求:
- 可以使用死循环控制服务端接收完消息继续等待接收下一个消息
- 客户端也可以使用死循环等待用户不断输入消息
- 客户端一旦输入了exit,则关闭客户端程序,并释放资源
使用while(true)即可实现,类似于UDP
/**
目标:实现多发和多收
*/
public class ClientDemo1 {
public static void main(String[] args) {
try {
System.out.println("====客户端启动===");
// 1、创建Socket通信管道请求有服务端的连接
// public Socket(String host, int port)
// 参数一:服务端的IP地址
// 参数二:服务端的端口
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();
}
// 关闭资源。
// socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
目标:开发Socket网络编程入门代码的服务端,实现接收消息
*/
public class ServerDemo2 {
public static void main(String[] args) {
try {
System.out.println("===服务端启动成功===");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
while (true) {
// 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);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4 TCP通信同时接收多个客户端消息
之前的TCP通信为什么不能同时与多个客户端通信? 答:单线程每次只能处理一个客户端的Socket通信。解决方法:引入多线程。
代码如下:(ClientDemo代码是一样的,不贴了)
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() + "下线了!!!");
}
}
}
/**
目标:实现服务端可以同时处理多个客户端的消息。
*/
public class ServerDemo2 {
public static void main(String[] args) {
try {
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();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
(有一个客户端被我叉掉了,所以没贴图了)
本次是如何实现服务端接收多个客户端的消息的?
答:主线程定义了循坏负责接收客户端Socket管道连接,每接收到一个Socket通信管道后分配一个独立的线程负责处理它。
5 使用线程池优化
4的通信架构存在一些问题:客户端与服务端的线程模型是N-N关系,客户端并发越多,系统瘫痪越快。
引入线程池处理多个客户端消息
代码如下,ClientDemo一样省略不写了
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() + "下线了!!!");
}
}
}
/**
目标:实现服务端可以同时处理多个客户端的消息。
*/
public class ServerDemo2 {
// 使用静态变量记住一个线程池对象
private static ExecutorService pool = new ThreadPoolExecutor(300,
1500, 6, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2)
, Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
try {
System.out.println("===服务端启动成功===");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(6666);
// a.定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
while (true) {
// 2、每接收到一个客户端的Socket管道,
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress()+ "它来了,上线了!");
// 任务对象负责读取消息。
Runnable target = new ServerReaderRunnable(socket);
pool.execute(target);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
本次使用线程池的优势是什么? 服务端可以服用线程处理多个客户端,可以避免系统瘫痪。适合客户端通信时间较短的场景。
四、TCP通信实战案例
1 即时通信
含义:指一个客户端的消息发出去,其他客户端可以接收到。(之前我们的消息都是发给服务端的,即时通信需要进行端口转发的设计思想)
代码及运行结果如下:
/**
拓展:即时通信
客户端:发消息的同时,随时有人发消息过来。
服务端:接收消息后,推送给其他所有的在线socket
*/
public class ClientDemo1 {
public static void main(String[] args) {
try {
System.out.println("====客户端启动===");
// 1、创建Socket通信管道请求有服务端的连接
// public Socket(String host, int port)
// 参数一:服务端的IP地址
// 参数二:服务端的端口
Socket socket = new Socket("127.0.0.1", 7070);
// 马上为客户端分配一个独立的线程负责读取它收到的消息
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();
}
// 关闭资源。
// socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public 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 msg;
while ((msg = br.readLine()) != null){
System.out.println("收到了: " + msg);
}
} catch (Exception e) {
System.out.println("服务端把你踢出去了~~");
}
}
}
/**
目标: 即时通信
*/
public class ServerDemo2 {
public static List<Socket> onLineSockets = new ArrayList<>();
public static void main(String[] args) {
try {
System.out.println("===服务端启动成功===");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7070);
// a.定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
while (true) {
// 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
Socket socket = serverSocket.accept();
System.out.println(socket.getRemoteSocketAddress()+ "它来了,上线了!");
// 把当前客户端管道Socket加入到在线集合中去
onLineSockets.add(socket);
// 3、开始创建独立线程处理socket
new ServerReaderThread(socket).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
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() + "下线了!!!");
// 从在线集合中抹掉本客户端socket
ServerDemo2.onLineSockets.remove(socket);
}
}
private void sendMsgToAll(String msg) {
try {
// 遍历全部的在线 socket给他们发消息
for (Socket onLineSocket : ServerDemo2.onLineSockets) {
// 除了自己的socket,其他socket我都发!!
if(onLineSocket != socket){
PrintStream ps = new PrintStream(onLineSocket.getOutputStream());
ps.println(msg);
ps.flush();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
(复习时思考一下:为什么这个叉掉客户端没有提示下线,如果要提示应该加在哪里)
即时通信是什么含义,要实现怎么样的设计?
- 即时通信,是指一个客户端的消息发出去,其他客户端可以接收到
- 即时通信需要进行端口转发的设计思想
- 服务端需要把在线的Socket管道存储起来
- 一旦收到一个消息推送给其他管道
2 模拟BS系统(目前暂时了解就行)
之前的客户端是什么样的?
- 答:其实就是CS架构,客户端需要我们自己开发实现
BS结构是什么样的,需要开发客户端吗?
- 答:浏览器访问服务端,不需要开发客户端
代码如下,可以自己去网页试一下:
/**
了解:BS-浏览器-服务器基本了解。
引入:
之前客户端和服务端都需要自己开发。也就是CS架构。
接下来模拟一下BS架构。
客户端:浏览器。(无需开发)
服务端:自己开发。
需求:在浏览器中请求本程序,响应一个网页文字给浏览器显示
*/
public class BSserverDemo {
// 使用静态变量记住一个线程池对象
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) {
try {
// 1.注册端口
ServerSocket ss = new ServerSocket(9080);
// 2.创建一个循环接收多个客户端的请求。
while(true){
Socket socket = ss.accept();
// 3.交给一个独立的线程来处理!
pool.execute(new ServerReaderRunnable(socket));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
// 浏览器 已经与本线程建立了Socket管道
// 响应消息给浏览器显示
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("<span style='color:red;font-size:90px'>《胡逸杰爱学习!加油(ง •_•)ง》 </span>");
ps.close();
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
}
}
}
五、总结
主要涉及Java网络编程的一些基础知识,挺有意思的!继续加油!