网络编程概述
什么是网络编程
可以让设备中的程序与网络上其他设备中的程序进行数据交互的技术(实现网络通信)。
基本的通信架构
基本的通信架构有2种形式:
- CS架构( Client客户端/Server服务端 )
- BS架构(Browser浏览器/Server服务端)。
CS架构
BS架构
无论是CS架构,还是BS架构的软件都必须依赖网络编程!
Java提供了哪些网络编程解决方案?
java.net.*包下提供了网络编程的解决方案!
网络编程三要素
- IP地址:设备在网络中的地址,是设备在网络中的唯一标识
- 端口:应用程序在设备中的唯一标识
- 协议:连接和数据在网络中传输的规则。
上图的运行流程是先去找IP地址(IP地址2),然后(IP地址2)再根据端口号找对应的程序(微信),传输协议是建立通信之间的协议(规则)。包括文本通过什么格式传输,图片是什么格式,视频是什么格式。
IP
IP地址
IP(Internet Protocol):全称”互联网协议地址”,是分配给上网设备的唯一标识。
目前,被广泛采用的IP地址形式有两种:IPv4、IPv6。
IPv4:
IPv4是Internet Protocol version 4的缩写,它使用32位地址,通常以点分十进制表示。
IPv6
IPv6是Internet Protocol version 6的缩写,它使用128位地址,号称可以为地球上的每一粒沙子编号。
IPv6分成8段,每段每四位编码成一个十六进制位表示, 每段之间用冒号(:)分开,将这种方式称为冒分十六进制。
IP域名(Domain Name)
用于在互联网上识别和定位网站的人类可读的名称。
例如:
www.baidu.com
www.itheima.com
DNS域名解析(Domain Name System)
是互联网中用于将域名转换为对应IP地址的分布式命名系统。它充当了互联网的“电话簿”,将易记的域名映射到数字化的IP地址,使得用户可以通过域名来访问网站和其他网络资源。
我们的新电脑不认识我们的域名,因为我们的新电脑肯定有宽带(电信,移动),宽带去找我们的运营商的解析器,记录了全球的IP和域名。
公网IP、内网IP
公网IP:是可以连接到互联网的IP地址;
内网IP:也叫局域网IP,是只能组织机构内部使用的IP地址;例如,192.168. 开头的就是常见的局域网地址,范围为192.168.0.0–192.168.255.255,专门为组织机构内部使用。
内网IP的目的
- 节省地址:192.168.0.0–192.168.255.255的每个公司够可用。
- 效率更高:内部使用,不用再去互联网上绕一大圈在去获取数据。
本机IP
127.0.0.1、localhost:代表本机IP,只会寻找当前程序所在的主机。
IP常用命令
ipconfig
:查看本机IP地址。
ipconfig /all
: 查看本机物理地址。
ping IP地址
:检查网络是否连通。
InetAddress类
InetAddress是java中的类,代表IP地址。
InetAddress的常用方法
InetAddress类的常用方法 | 说明 |
---|---|
public static InetAddress getLocalHost() throws UnknownHostException | 获取本机IP,返回一个InetAddress对象 |
public String getHostName() | 获取该ip地址对象对应的主机名。 |
public String getHostAddress() | 获取该ip地址对象中的ip地址信息。 |
public static InetAddress getByName(String host) throws UnknownHostException | 根据ip地址或者域名,返回一个inetAddress对象 |
public boolean isReachable(int timeout) throws IOException | 判断主机在指定毫秒内与该ip对应的主机是否能连通 |
import java.net.InetAddress;
public class InetAddressDemo1 {
public static void main(String[] args) {
// 目标:认识InetAddress获取本机IP对象和对方IP对象。
try {
// 1.获取本机IP对象
InetAddress ip1 = InetAddress.getLocalHost();
System.out.println(ip1.getHostName());// dlei
System.out.println(ip1.getHostAddress());// 192.168.**.**
// 2、获取对方IP对象
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());// www.baidu.com
System.out.println(ip2.getHostAddress());// 110.242.69.21
// 3、判断本机与对方主机是否互通
System.out.println(ip2.isReachable(5000));
// false 相当于ping命令
}catch (Exception e){
e.printStackTrace();
}
}
}
IP总结
说说网络通信至少需要几个要素?
IP、端口、协议
IP地址是做什么的,具体有几种?
定位网络上的设备的,有IPv4 , IPv6
如何查看本机IP地址,如何看是否与对方互通?
ipconfig
ping 192.168.10.23
本机IP是谁?
127.0.0.1或者是localhost
Java中,哪个类代表IP地址?
InetAddress
怎样获得本机IP对象?
InetAddress ip1 = InetAddress.getLocalHost();
端口
端口定义
用来标记标记正在计算机设备上运行的应用程序,被规定为一个 16 位的二进制,范围是 0~65535。
端口号的作用是什么?
唯一标识正在计算机设备上运行的进程(程序)
端口分类
周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用 80,FTP占用21) ,就比如报警电话110,你自己的手机号不可能是110。
注册端口:1024~49151,分配给用户进程或某些应用程序。
动态端口:49152到65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。
注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则报错。
一个设备中,能否出现2个应用程序的端口号一样,为什么?
不可以,如果一样会出现端口冲突错误。
协议
通信协议
网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。
为了让全球所有上网设备都能互联互通,需要指定一套统一的标准
开放式网络互联标准:OSI网络参考模型
OSI网络参考模型:全球网络互联标准。 (因为这个理论太过于理论没被采用)
TCP/IP网络模型:事实上的国际标准。(这个是实践)硬件商设计网卡就是使用这个模型。
上面这个图就是,我们这个电脑比如微信,
- 应用层定义比如传网页的数据格式(http),交给传输层
- 传输层使用UDP协议或者TCP协议
- 包装自己的IP信息
- 把上面的全部数据打包成bit流(101000110像这种)在网络中传输。
另一台电脑收到这个信息,然后从物理层到应用层一点一点的拆开。
传输层的通信协议
通信协议是什么?
计算机网络中,连接和通信数据的规则被称为网络通信协议。
这一层是网络工程师干的活。
分类
UDP(User Datagram Protocol):用户数据报协议。
TCP(Transmission Control Protocol) :传输控制协议。
UDP协议
用户数据报协议(User Datagram Protocol)协议有什么特点?
- UDP是面向无连接,不可靠传输的通信协议。
- 速度快,有大小限制一次最多发送64K,数据不安全,易丢失数据。
解释:不事先建立连接,数据按照包发,一包数据包含:自己的IP、端口、目的地IP、端口和数据(限制在64KB内)等。
发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的 。
主要应用于:通信效率高,视频直播,语音通话。因为画面模糊一点卡一点无所谓,语音有些字听不清也没关系。
TCP协议
特点:面向连接、可靠通信。
TCP的最终目的:要保证在不可靠的信道上实现可靠的数据传输。
TCP主要有三个步骤实现可靠传输:传输前 三次握手建立连接,传输数据进行确认,传输后 四次挥手断开连接。
传输前 三次握手建立可靠连接
可靠连接:确保通信的双方收发消息都是没问题的(全双工)
A --> B B确定A发消息没问题
A确定B收发消息和收消息都没问题 A <-- B
A --> B B确定A收消息没问题
传输后 四次挥手断开连接
目的:确保通信的双方收发消息都已经完成
特点:面向连接、可靠通信。
1.A发送断开连接 A --> B
3.A不会再发消息了,等待B接收完发送的消息 A <-- B 2.B给A发消息说你发的消息我收到了,你不需要重复发了。(只要B不回信息A就认为你没收到会重复发)
A <-- B 4.我已经接受完你发的所有消息了,断开吧,收到请回答
5.A知道B发完了,发送收到我要断开了 A --> B 5.收到
TCP协议有哪些特点?
- TCP是一种面向连接的可靠通信协议
- 传输前,采用“三次握手”方式建立连接,点对点的通信。
- 在连接中可进行大数据量的传输。
- 传输后,采用“四次挥手”方式断开连接,确保消息全部收发完毕。
- 通信效率相对较低,可靠性相对较高。
java实现UDP通信
快速入门
UDP通信的实现
特点:无连接、不可靠通信。
不事先建立连接;发送端每次把要发送的数据(限制在64KB内)、接收端IP、等信息封装成一个数据包,发出去就不管了。
Java提供了一个java.net.DatagramSocket类来实现UDP通信。
DatagramSocket: 用于创建客户端、服务端
构造器 | 说明 |
---|---|
public DatagramSocket() | 创建客户端的Socket对象, 系统会随机分配一个端口号。 |
public DatagramSocket(int port) | 创建服务端的Socket对象, 并指定端口号 |
方法 | 说明 |
---|---|
public void send(DatagramPacket dp) | 发送数据包 |
public void receive(DatagramPacket p) | 使用数据包接收数据 |
DatagramPacket:创建数据包
构造器 | 说明 |
---|---|
public DatagramPacket(byte[] buf, int length, InetAddress address, int port) | 创建发出去的数据包对象 |
public DatagramPacket(byte[] buf, int length) | 创建用来接收数据的数据包 |
方法 | 说明 |
---|---|
public int getLength() | 获取数据包,实际接收到的字节个数 |
客户端实现步骤
- 创建DatagramSocket对象(客户端对象)--------> 扔韭菜的人
- 创建DatagramPacket对象封装需要发送的数据(数据包对象)--------> 韭菜盘子
- 使用DatagramSocket对象的send方法,传入DatagramPacket对象 -------->开始抛出韭菜
- 释放资源
服务端实现步骤
- 创建DatagramSocket对象并指定端口(服务端对象)--------> 接韭菜的人
- 创建DatagramPacket对象接收数据(数据包对象) --------> 韭菜盘子
- 使用DatagramSocket对象的receive方法,传入DatagramPacket对象 --------> 开始接收韭菜
- 释放资源
服务端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServerDemo2 {
public static void main(String[] args) throws Exception {
// 目标:完成UDP通信一发一收:服务端开发。
System.out.println("==服务端启动了===");
// 1、创建接收端对象,注册端口。(接韭菜的人)
DatagramSocket socket = new DatagramSocket(8080);
// 2、创建一个数据包对象负责接收数据。(韭菜盘子)
byte[] buf = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
// 3、接收数据,将数据封装到数据包对象的字节数组中去
socket.receive(packet);
// 4、看看数据是否收到了
int len = packet.getLength(); // 获取当前收到的数据长度。
String data = new String(buf, 0 , len);
System.out.println("服务端收到了:" + data);
// 获取对方的ip对象和程序端口
String ip = packet.getAddress().getHostAddress();
int port = packet.getPort();
System.out.println("对方ip:" + ip + " 对方端口:" + port);
// ==服务端启动了===
// 服务端收到了:我是客户端,约你今晚啤酒、龙虾、小烧烤
// 对方ip:192.168.1.10 对方端口:61807
socket.close();
}
}
客户端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPClientDemo1 {
public static void main(String[] args) throws Exception {
// 目标:完成UDP通信一发一收:客户端开发
System.out.println("===客户端启动==");
// 1、创建发送端对象(代表抛韭菜的人)
DatagramSocket socket = new DatagramSocket(); // 随机端口
// 2、创建数据包对象封装要发送的数据。(韭菜盘子)
byte[] bytes = "我是客户端,约你今晚啤酒、龙虾、小烧烤".getBytes();
/**
* public DatagramPacket(byte[] buf, int length,
* InetAddress address, int port)
* 参数一:发送的数据,字节数组(韭菜)
* 参数二:发送的字节长度。
* 参数三:目的地的IP地址。
* 参数四:服务端程序的端口号
*/
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 8080);
// 3、让发送端对象发送数据包的数据
socket.send(packet);
// ===客户端启动==
socket.close();
}
}
总结
实现UDP通信,如何创建客户端、服务端对象?
public DatagramSocket():创建发送端的Socket对象
public DatagramSocket(int port):创建接收端的Socket对象
数据包对象是哪个?
DatagramPacket
如何发送、接收数据?
使用DatagramSocket的如下方法:
public void send(DatagramPacket dp):发送数据包
public void receive(DatagramPacket dp) :接收数据包
多发多收
客户端可以反复发送数据
客户端实现步骤
- 创建DatagramSocket对象(发送端对象
- 使用while死循环不断的接收用户的数据输入,如果用户输入的exit则退出程序
- 如果用户输入的不是exit, 把数据封装成DatagramPacket
- 使用DatagramSocket对象的send方法将数据包对象进行发送
- 释放资源
接收端可以反复接收数据
接收端实现步骤
- 创建DatagramSocket对象并指定端口(接收端对象)--------> 接韭菜的人
- 创建DatagramPacket对象接收数据(数据包对象)--------> 韭菜盘子
- 使用DatagramSocket对象的receive方法传入DatagramPacket对象-------->开始接收韭菜
- 使用while死循环不断的进行第3步
UDP的接收端为什么可以接收很多发送端的消息?
接收端只负责接收数据包,无所谓是哪个发送端的数据包。
服务端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServerDemo2 {
public static void main(String[] args) throws Exception {
// 目标:完成UDP通信多发多收:服务端开发。
System.out.println("==服务端启动了===");
// 1、创建接收端对象,注册端口。(接韭菜的人)
DatagramSocket socket = new DatagramSocket(8080);
// 2、创建一个数据包对象负责接收数据。(韭菜盘子)
byte[] buf = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
while (true) {
// 3、接收数据,将数据封装到数据包对象的字节数组中去
socket.receive(packet); // 等待式接收数据。
// 4、看看数据是否收到了
int len = packet.getLength(); // 获取当前收到的数据长度。
String data = new String(buf, 0 , len);
System.out.println("服务端收到了:" + data);
// 获取对方的ip对象和程序端口
String ip = packet.getAddress().getHostAddress();
int port = packet.getPort();
System.out.println("对方ip:" + ip + " 对方端口:" + port);
System.out.println("----------------------------------------------");
}
}
}
==服务端启动了===
服务端收到了:你好
对方ip:192.168.1.10 对方端口:56056
----------------------------------------------
服务端收到了:你好呀
对方ip:192.168.1.10 对方端口:56056
----------------------------------------------
客户端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
// 都听的懂,但是记不住!
public class UDPClientDemo1 {
public static void main(String[] args) throws Exception {
// 目标:完成UDP通信多发多收:客户端开发
System.out.println("===客户端启动==");
// 1、创建发送端对象(代表抛韭菜的人)
DatagramSocket socket = new DatagramSocket(); // 随机端口
Scanner sc = new Scanner(System.in);
while (true) {
// 2、创建数据包对象封装要发送的数据。(韭菜盘子)
System.out.println("请说:");
String msg = sc.nextLine();
// 如果用户输入的是 exit,则退出
if ("exit".equals(msg)) {
System.out.println("===客户端退出==");
socket.close();
break;
}
byte[] bytes = msg.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length,
InetAddress.getLocalHost(), 8080);
// 3、让发送端对象发送数据包的数据
socket.send(packet);
}
}
}
===客户端启动==
请说:
你好
请说:
你好呀
java实现TCP通信
快速入门
TCP通信的实现
特点:面向连接、可靠通信。
通信双方事先会采用“三次握手”方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端。
Java提供了一个java.net.Socket类来实现TCP通信。
TCP通信的实现一发一收-客户端开发
客户端程序就是通过java.net包下的Socket类来实现的。
构造器 | 说明 |
---|---|
public Socket(String host , int port) | 根据指定的服务器ip、端口号请求与服务端建立连接,连接通过,就获得了客户端socket |
方法 | 说明 |
---|---|
public OutputStream getOutputStream() | 获得字节输出流对象 |
public InputStream getInputStream() | 获得字节输入流对象 |
TCP通信的实现一发一收-客户端开发步骤
- 创建客户端的Socket对象,请求与服务端的连接。
- 使用socket对象调用getOutputStream()方法得到字节输出流。
- 使用字节输出流完成数据的发送。
- 释放资源:关闭socket管道。
TCP通信的实现一发一收-服务端开发
服务端是通过java.net包下的ServerSocket类来实现的。
ServerSocket
构造器 | 说明 |
---|---|
public ServerSocket(int port) | 为服务端程序注册端口 |
方法 | 说明 |
---|---|
public Socket accept() | 阻塞等待客户端的连接请求,一旦与某个客户端成功连接,则返回服务端这边的Socket对象。 |
TCP通信的实现一发一收-服务端开发步骤
- 创建ServerSocket对象,注册服务端端口。
- 调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象。
- 通过Socket对象调用getInputStream()方法得到字节输入流、完成数据的接收。
- 释放资源:关闭socket管道
服务端
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
// 目标:实现TCP通信下一发一收:服务端开发。
System.out.println("服务端启动了...");
// 1、创建服务端ServerSocket对象,绑定端口号,监听客户端连接
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=" + id + ",收到的客户端msg=" + msg);
// 6、客户端的ip和端口(谁给我发的)
System.out.println("客户端的ip=" + socket.getInetAddress().getHostAddress());
System.out.println("客户端的端口=" + socket.getPort());
}
}
服务端启动了...
id=1,收到的客户端msg=我想你了,你在哪儿?
客户端的ip=127.0.0.1
客户端的端口=4391
客户端
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
// 目标:实现TCP通信下一发一收:客户端开发。
System.out.println("客户端启动....");
// 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("我想你了,你在哪儿?");
// 4、关闭资源。
socket.close();
}
}
客户端启动....
总结
TCP通信,客户端的代表类是谁?
Socket类
public Socket(String host , int port)
TCP通信,如何使用Socket管道发送、接收数据 ?
OutputStream getOutputStream():获得字节输出流对象(发)
InputStream getInputStream():获得字节输入流对象(收)
TCP通信服务端用的类是谁?
ServerSocket类,注册端口。
调用accept()方法阻塞等待接收客户端连接。得到Socket对象。
多发多收
案例:使用TCP通信实现:多发多收消息
客户端使用死循环,让用户不断输入消息。
服务端也使用死循环,控制服务端程序收完消息后,继续去接收下一个消息。
总结
本次多发多收是如何实现的?
客户端使用循环反复地发送消息。
服务端使用循环反复地接收消息。
服务端
package com.itheima.demo5tcp2;
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
// 目标:实现TCP通信下多发多收:服务端开发。
System.out.println("服务端启动了...");
// 1、创建服务端ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket ss = new ServerSocket(9999);
// 2、调用accept方法,阻塞等待客户端连接,一旦有客户端链接会返回一个Socket对象
Socket socket = ss.accept();
// 3、获取输入流,读取客户端发送的数据
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成特殊数据输入流
DataInputStream dis = new DataInputStream(is);
while (true) {
// 5、读取数据
String msg = dis.readUTF(); // 等待读取客户端发送的数据
System.out.println("收到的客户端msg=" + msg);
// 6、客户端的ip和端口(谁给我发的)
System.out.println("客户端的ip=" + socket.getInetAddress().getHostAddress());
System.out.println("客户端的端口=" + socket.getPort());
System.out.println("--------------------------------------------------");
}
}
}
服务端启动了...
收到的客户端msg=你好
客户端的ip=127.0.0.1
客户端的端口=5694
--------------------------------------------------
收到的客户端msg=去吃饭
客户端的ip=127.0.0.1
客户端的端口=5694
--------------------------------------------------
客户端
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
// 目标:实现TCP通信下多发多收:客户端开发。
System.out.println("客户端启动....");
// 1、常见Socket管道对象,请求与服务端的Socket链接。可靠链接
Socket socket = new Socket("127.0.0.1", 9999);
// 2、从socket通信管道中得到一个字节输出流。
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 ("exit".equals(msg)) {
System.out.println("退出成功!");
dos.close(); // 关闭输出流
socket.close(); // 关闭socket
break;
}
dos.writeUTF(msg); // 发送数据
dos.flush();
}
}
}
客户端启动....
请说:
你好
请说:
去吃饭
同时接收多个客户端的消息
目前我们开发的服务端程序,是否可以支持同时与多个客户端通信 ?
不可以
因为服务端现在只有一个主线程,只能处理一个客户端的消息。
总结
本次是如何实现服务端同时接收多个客户端的消息的?
主线程定义了循环负责接收客户端Socket管道连接
每接收到一个Socket通信管道后分配一个独立的线程负责处理它。
同一程序打开多个控制台
运行服务层
服务端启动了...
一个客户端上线了:127.0.0.1
一个客户端上线了:127.0.0.1
收到的客户端msg=我是第一条线程
客户端的ip=127.0.0.1
客户端的端口=6187
--------------------------------------------------
收到的客户端msg=我是第二条线程
客户端的ip=127.0.0.1
客户端的端口=7067
--------------------------------------------------
运行一次客户端
客户端启动....
请说:
我是第一条线程
运行两次客户端
客户端启动....
请说:
我是第二条线程
代码实现
服务端
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
// 目标:实现TCP通信下多发多收:服务端开发。支持多个客户端开发。
System.out.println("服务端启动了...");
// 1、创建服务端ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket ss = new ServerSocket(9999);
while (true) {
// 2、调用accept方法,阻塞等待客户端连接,一旦有客户端链接会返回一个Socket对象
Socket socket = ss.accept();
System.out.println("一个客户端上线了:" + socket.getInetAddress().getHostAddress());
// 3、把这个客户端管道交给一个独立的子线程专门负责接收这个管道的消息。
new ServerReader(socket).start();
}
}
}
调用线程
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.Socket;
public class ServerReader extends Thread{
private Socket socket;
public ServerReader(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 读取管道的消息
// 3、获取输入流,读取客户端发送的数据
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成特殊数据输入流
DataInputStream dis = new DataInputStream(is);
while (true) {
// 5、读取数据
String msg = dis.readUTF(); // 等待读取客户端发送的数据
System.out.println("收到的客户端msg=" + msg);
// 6、客户端的ip和端口(谁给我发的)
System.out.println("客户端的ip=" + socket.getInetAddress().getHostAddress());
System.out.println("客户端的端口=" + socket.getPort());
System.out.println("--------------------------------------------------");
}
} catch (Exception e) {
System.out.println("客户端下线了:"+ socket.getInetAddress().getHostAddress());
}
}
}
客户端(设置同一程序打开多个控制台)
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
// 目标:实现TCP通信下多发多收:支持多个客户端开发。
System.out.println("客户端启动....");
// 1、常见Socket管道对象,请求与服务端的Socket链接。可靠链接
Socket socket = new Socket("127.0.0.1", 9999);
// 2、从socket通信管道中得到一个字节输出流。
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 ("exit".equals(msg)) {
System.out.println("退出成功!");
dos.close(); // 关闭输出流
socket.close(); // 关闭socket
break;
}
dos.writeUTF(msg); // 发送数据
dos.flush();
}
}
}
其他应用:B/S架构的原理
要求从浏览器中访问服务器
并立即让服务器响应一个很简单的网页给浏览器展示
网页内容就是“听黑马磊哥讲Java”
拓展知识
每次请求都开一个新线程,到底好不好?
高并发时,容易宕机!
使用线程池进行优化
总结
1、BS架构的基本原理是什么?
客户端使用浏览器发起请求(不需要开发客户端)
服务端必须按照HTTP协议响应数据。
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
public class ServerDemo {
public static void main(String[] args) throws Exception {
// 目标:BS架构的原理理解
System.out.println("服务端启动了...");
// 1、创建服务端ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket ss = new ServerSocket(8080);
// 创建线程池
ExecutorService pool = new ThreadPoolExecutor(3, 10, 10, TimeUnit.SECONDS
, new ArrayBlockingQueue<>(100), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
while (true) {
// 2、调用accept方法,阻塞等待客户端连接,一旦有客户端链接会返回一个Socket对象
Socket socket = ss.accept();
System.out.println("一个客户端上线了:" + socket.getInetAddress().getHostAddress());
// 3、把这个客户端管道包装成一个任务交给线程池处理
pool.execute(new ServerReaderRunnable(socket));
}
}
}
ServerReaderRunnable.java
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
public class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 给当前对应的浏览器管道响应一个网页数据回去。
OutputStream os = socket.getOutputStream();
// 通过字节输出流包装写出去数据给浏览器
// 把字节输出流包装成打印流。
PrintStream ps = new PrintStream(os);
// 写响应的网页数据出去
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=utf-8");
ps.println(); // 必须换一行
ps.println("<html>");
ps.println("<head>");
ps.println("<meta charset='utf-8'>");
ps.println("<title>");
ps.println("黑马Java磊哥的视频");
ps.println("</title>");
ps.println("</head>");
ps.println("<body>");
ps.println("<h1 style='color:red;font-size=20px'>听黑马Java磊哥的视频</h1>");
// 响应一个黑马程序员的log展示
ps.println("<img src='https://www.itheima.com/images/logo.png'>");
ps.println("</body>");
ps.println("</html>");
ps.close();
socket.close();
} catch (Exception e) {
System.out.println("客户端下线了:"+ socket.getInetAddress().getHostAddress());
}
}
}
服务端启动了...
一个客户端上线了:127.0.0.1
一个客户端上线了:127.0.0.1
一个客户端上线了:127.0.0.1
一个客户端上线了:127.0.0.1
一个客户端上线了:127.0.0.1
一个客户端上线了:127.0.0.1
一个客户端上线了:127.0.0.1
综合案例
项目介绍,前置知识讲解
完成局域网内沟通软件的开发
时间相关的获取方案
LocalDate:代表本地日期(年、月、日、星期)
LocalTime:代表本地时间(时、分、秒、纳秒)
LocalDateTime:代表本地日期、时间(年、月、日、星期、时、分、秒、纳秒)
它们获取对象的方案
方法名 | 示例 |
---|---|
public static Xxxx now(): 获取系统当前时间对应的该对象 | LocaDate ld = LocalDate.now(); LocalTime lt = LocalTime.now(); LocalDateTime ldt = LocalDateTime.now(); |
LocalDateTime的常用API(可以处理年、月、日、星期、时、分、秒、纳秒等信息)
方法名 | 说明 |
---|---|
getYear、getMonthValue、getDayOfMonth、getDayOfYeargetDayOfWeek、getHour、getMinute、getSecond、getNano | 获取年月日、时分秒、纳秒等 |
withYear、withMonth、withDayOfMonth、withDayOfYearwithHour、withMinute、withSecond、withNano | 修改某个信息,返回新日期时间对象 |
plusYears、plusMonths、plusDays、plusWeeksplusHours、plusMinutes、plusSeconds、plusNanos | 把某个信息加多少,返回新日期时间对象 |
minusYears、minusMonths、minusDays、minusWeeksminusHours、minusMinutes、minusSeconds、minusNanos | 把某个信息减多少,返回新日期时间对象 |
equals isBefore isAfter | 判断2个时间对象,是否相等,在前还是在后 |
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
public class Test1 {
public static void main(String[] args) {
// 目标:掌握java提供的获取时间的方案。
// JDK 8之前的方案: Date 获取此刻日期时间 老项目还有。
Date d = new Date();
System.out.println(d);// Tue May 13 13:25:15 CST 2025
// 格式化:SimpleDateFormat 简单日期格式化,格式化日期对象成为我们喜欢的格式。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEE a");
String result = sdf.format(d);
System.out.println(result); // 2025年05月13日 13:25:15 周二 下午
System.out.println("---------------------------");
// JDK 8之后的方案: LocalDate LocalTime LocalDateTime 获取此刻日期时间 新项目推荐。
// 获取此刻日期时间对象
LocalDateTime now = LocalDateTime.now();
System.out.println(now);// 2025-05-13T13:25:15.551119400
System.out.println(now.getYear());// 2025
// 获取当前是第几天
System.out.println(now.getDayOfYear());// 133
LocalDateTime now2 = now.plusSeconds(60); // 60秒后
System.out.println(now);// 2025-05-13T13:25:15.551119400
System.out.println(now2);// 2025-05-13T13:26:15.551119400
// 格式化:DateTimeFormatter
// 1、创建一个格式化对象
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss EEE a");
// 2、格式化now对象的时间
String result2 = dtf.format(now);
System.out.println(result2);// 2025/05/13 13:25:15 周二 下午
}
}
字符串的高效操作方案
- 对于字符串相关的操作,如频繁的拼接、修改等,建议用StringBuidler,效率更高!
- 注意:如果操作字符串较少,或者不需要操作,以及定义字符串变量,还是建议用String。
StringBuilder
- StringBuilder代表可变字符串对象,相当于是一个容器,它里面装的字符串是可以改变的,就是用来操作字符串的。
- 好处:StringBuilder比String更适合做字符串的修改操作,效率会更高,代码也会更简洁。
构造器 | 说明 |
---|---|
public StringBuilder() | 创建一个空白的可变的字符串对象,不包含任何内容 |
public StringBuilder(String str) | 创建一个指定字符串内容的可变字符串对象 |
方法名称 | 说明 |
---|---|
public StringBuilder append(任意类型) | 添加数据并返回StringBuilder对象本身 |
public StringBuilder reverse() | 将对象的内容反转 |
public int length() | 返回对象内容长度 |
public String toString() | 通过toString()就可以实现把StringBuilder转换为String |
public class Test2 {
public static void main(String[] args) {
// 目标:高效拼接字符串。
// + 号拼接字符串内容,如果是大量拼接,效率极差!
// String的对象时不可变变量: 共享数据性能可以,但是修改数据性能差!
// String s = "";
// for (int i = 0; i < 1000000 ; i++) {
// s = s + "abc";
// }
// System.out.println(s);
// 定义字符串可以使用String类型,但是操作字符串建议大家用StringBuilder(性能好)
StringBuilder sb = new StringBuilder(); // StringBuilder对象是可变内容的容器 sb = "";
for (int i = 0; i < 1000000 ; i++) {
sb.append("abc");
}
System.out.println(sb);
// StringBuilder只是拼接字符串的手段,结果还是要恢复成字符串(目的)
String s = sb.toString();
System.out.println(s);
StringBuilder sb2 = new StringBuilder();
String result = sb2.append("张三").append("李四").append("王五").toString();
System.out.println(result);
}
}
BigDecimal
用于解决浮点型运算时,出现结果失真的问题。
double a = 0.1;
double b = 0.2;
System.out.println(a + b); // 0.30000000000000004
// 为什么会失真
// 浮点数在内存中以二进制形式存储,所以浮点数在计算机中无法精确表示。
BigDecimal的常见构造器、常用方法
构造器 | 说明 |
---|---|
public BigDecimal(double val) 注意:不推荐使用这个 | 将 double转换为 BigDecimal |
public BigDecimal(String val) | 把String转成BigDecimal |
方法名 | 说明 |
---|---|
public static BigDecimal valueOf(double val) | 转换一个 double成 BigDecimal |
public BigDecimal add(BigDecimal b) | 加法 |
public BigDecimal subtract(BigDecimal b) | 减法 |
public BigDecimal multiply(BigDecimal b) | 乘法 |
public BigDecimal divide(BigDecimal b) | 除法 |
public BigDecimal divide (另一个BigDecimal对象,精确几位,舍入模式) | 除法、可以控制精确到小数几位 |
public double doubleValue() | 将BigDecimal转换为double |
总结
BigDecimal的作用是什么 ?
解决浮点型运算时,出现结果失真的问题。
应该如何把浮点型转换成BigDecimal的对象?
BigDecimal b1 = BigDecimal.valueOf(0.1)
BigDecimal提供了哪些常见的方法 ?
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Test3 {
public static void main(String[] args) {
// 目标:掌握BigDecimal解决小数运算结果失真问题。
double a = 0.1;
double b = 0.2;
System.out.println(a + b); // 0.30000000000000004
// 为什么会失真
// 浮点数在内存中以二进制形式存储,所以浮点数在计算机中无法精确表示。
// 如何解决呢? 使用BigDecimal
// 1、把小数包装成BigDecimal对象来运算才可以。
// 必须使用 public BigDecimal(String val) 字符串构造器才能解决失真问题
// BigDecimal a1 = new BigDecimal(Double.toString(a));
// BigDecimal b1 = new BigDecimal(Double.toString(b));
// 优化方案,可以直接调用valueOf方法,内部使用的就是public BigDecimal(String val) 字符串构造器
BigDecimal a1 = BigDecimal.valueOf(a);
BigDecimal b1 = BigDecimal.valueOf(b);
BigDecimal c1 = a1.add(b1); // 解决精度问题的手段
double result = c1.doubleValue(); // 目的 把BigDecimal对象转成double类型
System.out.println(result);// 0.3
System.out.println("------------");
BigDecimal i = BigDecimal.valueOf(0.1);
BigDecimal j = BigDecimal.valueOf(0.3);
// 除法
// 默认除法运算精度 divide(BigDecimal divisor, int scale, RoundingMode roundingMode)
// scale:精度
// roundingMode:舍入模式
BigDecimal k = i.divide(j, 2, RoundingMode.HALF_UP); // 抛异常
System.out.println(k);
}
}
项目实现思路分析
综合案例请参考我的java专栏。