网络编程可以让程序与网络上的其他设备中的程序进行数据交互。
网络通信基本模式:Client-Server(CS) 、rowser/Server(BS)
三要素:IP地址、端口、协议。
IP地址:设备在网络中的地址,是唯一的标识。常见的 IP 分类为:
地址形式:公网地址、和私有地址(局域网使用),192.168.
开头的就是常见的局域网地址,范围即为 192.168.0.0 ~ 192.168.255.255
,专门为组织机构内部使用。
IP常用命令:
ipconfig
:查看本机IP地址ping IP地址
:检查网络是否连通特殊 IP 地址:如本机IP 127.0.0.1
或者 localhost
称为回送地址也可称本地回环地址,只会寻找当前所在本机。
IP地址操作类 InetAddress:此类表示 Internet 协议(IP)地址。常用 API 如下:
public class InetAddressDemo01 {
public static void main(String[] args) throws Exception {
// 1.获取本机地址对象。
InetAddress ip1 = InetAddress.getLocalHost(); // DESKTOP-Q8LL1E1/192.168.171.1
System.out.println(ip1.getHostName());// 192.168.171.1
System.out.println(ip1.getHostAddress());// DESKTOP-Q8LL1E1
// 2.获取域名ip对象
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName()); // www.baidu.com
System.out.println(ip2.getHostAddress()); // 182.61.200.6
// 3.获取公网IP对象。
InetAddress ip3 = InetAddress.getByName("112.80.248.76");
System.out.println(ip3.getHostName()); // 112.80.248.76
System.out.println(ip3.getHostAddress()); // 112.80.248.76
// 4.判断是否在指定毫秒内连通该IP地址对应的主机
System.out.println(ip3.isReachable(5000));
}
}
端口:应用程序在设备中唯一的标识,被规定为一个 16 位的二进制,范围是 0~65535。
端口类型:
注:一个设备中不能出现两个程序的端口号一样,不同设备的相同程序,端口号要一样。
协议:数据在网络中传输的规则,常见的协议有 UDP 协议和 TCP 协议。
协议特点:
协议通信场景: 语音通话,视频会话等。
UDP协议比喻:
协议特点:
协议通信场景: 对信息安全要求较高的场景,例如:文件下载、金融等数据通信。
UDP是一种无连接、不可靠传输的协议,其将数据源IP、目的地IP和端口以及数据封装成数据包,大小限制在64KB内,直接发送出去即可。
发送消息:
① 创建 DatagramSocket 对象(发送端对象)
② 创建 DatagramPacket 对象封装需要发送的数据(数据包对象)
③ 使用 DatagramSocket 对象的 send 方法传入 DatagramPacket 对象
④ 释放资源
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
// 1. 创建发送端对象,发送端自带默认端口号 (人)
DatagramSocket socket = new DatagramSocket();
// 2. 创建一个数据包对象封装数据(韭菜)
byte[] bytes = "甜晕了".getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 8888);
// 接收端需指定端口号8888
// 3. 发送数据出去
socket.send(packet);
// 4. 释放资源
socket.close();
}
}
接收消息:
① 创建 DatagramSocket 对象并指定端口(接收端对象)
② 创建 DatagramPacket 对象接收数据(数据包对象)
③ 使用 DatagramSocket 对象的 receive 方法传入 DatagramPacket 对象
④ 释放资源
public class ServerDemo2{
public static void main(String[] args) throws Exception {
// 1. 创建接收端对象:注册端口(人)
DatagramSocket socket = new DatagramSocket(8888);
// 2. 创建一个数据包对象接收数据(韭菜)
byte[] buffer = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// 3. 等待接收数据。
socket.receive(packet);
// 4. 取出数据即可
int len = packet.getLength(); // 读取多少倒出多少
String rs = new String(buffer, 0, len);
System.out.println("收到了:" + rs);
String ip = packet.getSocketAddress().toString(); // 获取发送端的 ip 和端口
System.out.println("对方地址:" + ip);
int port = packet.getPort();
System.out.println("对方端口:" + port);
// 5. 释放资源
socket.close();
}
}
输出:
在上文案例的基础上添加 while 循环。
客户端:
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
// 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);
}
}
}
注:接收端只负责接收数据包,无所谓是哪个发送端的数据包。
输出:
idea 默认一个程序开启一次,当重复运行同一个程序时,会有如下提示:
因此,如果想实现客户端多开,需要修改配置,如下:
第一步:点击 Edit Configurations… 按钮。
第二步:点击 Modify options 修改参数。
点击 Allow multiple instances 选项,即可实现客户端多开功能:
其次,如果客户端指定端口号,会抛出端口号已经使用的异常。
因此,需要不用写指定端口,使用其默认端口号,不会导致端口号冲突。
TCP 是一种面向连接,安全、可靠的传输数据的协议。 数据传输前,采用 “三次握手” 方式,点对点通信,是可靠的。 并且在连接中可进行大数据量的传输。
注意:在 java 中只要是使用 java.net.Socket 类实现通信,其底层使用了 TCP 协议。
发送消息:
① 创建客户端的 Socket 对象,请求与服务端的连接。
② 使用 Socket 对象调用 getOutputStream() 方法得到字节输出流。
③ 使用字节输出流完成数据的发送。
④ 释放资源:关闭 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", 7777);
// 2、从socket通信管道中得到一个字节输出流 负责发送数据
OutputStream os = socket.getOutputStream();
// 3、把低级的字节流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);
// 4、发送消息
dos.writeUTF("我是TCP的客户端,我已经与你对接,并发出邀请:约吗?");
dos.close();
// 关闭资源
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
接收消息:
① 创建 ServerSocket 对象,注册服务端端口。
② 调用 ServerSocket 对象的 accept() 方法,等待客户端的连接,并得到 Socket 管道对象。
③ 通过 Socket 对象调用 getInputStream() 方法得到字节输入流、完成数据的接收。
④ 释放资源:关闭 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、把原始字节输入流包装成 数据输入流进行消息的接收
DataInputStream dis = new DataInputStream(is);
// 5、使用数据输入流读取客户端发送过来的消息
String msg = dis.readUTF();
System.out.println(msg);
// 补充:获取客户端的地址
System.out.println(socket.getRemoteSocketAddress());
// 6、关闭流
dis.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
TCP 通信原理:
客户端怎么发,服务端就应该怎么收。
客户端如果没有消息,服务端会进入阻塞等待。
Socket 一方关闭或者出现异常、对方 Socket 也会失效或者出错。
步骤:
① 客户端也可以使用死循环等待用户不断输入消息。
② 可以使用死循环控制服务端收完消息继续等待接收下一个消息。
③ 客户端一旦输入了 exit ,则关闭客户端程序,并释放资源。
客户端:
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、把低级的字节流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("请说:");
String msg = sc.nextLine();
if("exit".equals(msg)){
System.out.println("离线成功");
// 关闭资源
dos.close();
socket.close();
break;
}
// 4、发送消息
dos.writeUTF(msg);
dos.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
服务端:
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("===服务端启动成功===");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// 2、必须调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
Socket socket = serverSocket.accept();
// 3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成缓冲字符输入流进行消息的接收
DataInputStream dis = new DataInputStream(is);
while(true){
// 5、使用数据输入流读取客户端发送过来的消息
String msg = dis.readUTF();
System.out.println(socket.getRemoteSocketAddress()+ "说:" + msg);
}
}
}
注意:若此时关闭客服端,服务端会报错!原因客户端与服务端建立的是同一个管道。
可以通过异常捕获来解决以上问题:
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("===服务端启动成功===");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// 2、必须调用accept方法:等待接收客户端的Socket连接请求,建立Socket通信管道
Socket socket = serverSocket.accept();
// 3、从socket通信管道中得到一个字节输入流
InputStream is = socket.getInputStream();
// 4、把字节输入流包装成缓冲字符输入流进行消息的接收
DataInputStream dis = new DataInputStream(is);
while(true){
try {
// 5、使用数据输入流读取客户端发送过来的消息
String msg = dis.readUTF();
System.out.println(socket.getRemoteSocketAddress()+ "说:" + msg);
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress() + "离线了!");
dis.close();
socket.close();
break;
}
}
}
}
注意:服务端不可以同时接收多个客户端的消息,因为服务端现在只有一个线程,只能与一个客户端建立通信管道。
对于上文案例的问题,可以通过多线程的方式来解决。
主线程定义了循环负责接收客户端 Socket 管道连接,每接收到一个Socket通信管道后分配一个独立的线程负责处理它。
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()+ " ta来了,上线了!");
// 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、把字节输入流包装成缓冲字符输入流进行消息的接收
DataInputStream dis = new DataInputStream(is);
// 5、按照行读取消息
while(true){
try {
// 5、使用数据输入流读取客户端发送过来的消息
String msg = dis.readUTF();
System.out.println(socket.getRemoteSocketAddress()+ " 说:" + msg);
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress() + " 离线了!");
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出:
TCP 通信,需要实现 端口转发 的思想,即客户端是不能和客户端直接通信的,需要先发送到服务端,然后通过服务端,转发到客户端。
此外,还需要使用集合存储在线 socket,方便为哪些 socket 推送群聊信息。
实现:
public class Client {
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);
// F. 创建一个独立的线程,负责随机从 socket 中接收服务端发送过来的消息。
new ClientReaderThread(socket).start();
// 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();
break;
}
// 4、发送消息
dos.writeUTF(msg);
dos.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// E. 创建 客户端线程 接收其他客户端的信息
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、把字节输入流包装成缓冲字符输入流进行消息的接收
DataInputStream dis = new DataInputStream(is);
// 5、按照行读取消息
while(true){
try {
// 5、使用数据输入流读取客户端发送过来的消息
String msg = dis.readUTF();
System.out.println(socket.getRemoteSocketAddress()+ " 说:" + msg);
} catch (IOException e) {
System.out.println("自己离线了!");
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Server {
// A. 在线 socket 集合
public static List<Socket> onLineSockets = new ArrayList<>();
public static void main(String[] args) {
try {
System.out.println("===服务端启动成功===");
// 1、注册端口
ServerSocket serverSocket = new ServerSocket(7777);
// 定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
while (true) {
// 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
Socket socket = serverSocket.accept();
// B. 加入socket集合中
onLineSockets.add(socket);
System.out.println(socket.getRemoteSocketAddress()+ " ta来了,上线了!");
// 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、把字节输入流包装成缓冲字符输入流进行消息的接收
DataInputStream dis = new DataInputStream(is);
// 5、按照行读取消息
while(true){
try {
// 5、使用数据输入流读取客户端发送过来的消息
String msg = dis.readUTF();
System.out.println(socket.getRemoteSocketAddress()+ " 说:" + msg);
// D. 把这个消息分发给全部客户端进行接收!
sendMsgToAll(msg);
} catch (IOException e) {
System.out.println(socket.getRemoteSocketAddress() + " 离线了!");
// C. 在线 socket 集合删除当前 socket
Server.onLineSockets.remove(socket);
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 发送全部所有的socket的管道接收
*/
private void sendMsgToAll(String msg) throws IOException {
for (Socket onLineSocket : Server.onLineSockets) {
OutputStream os = onLineSocket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF(msg);
dos.flush();
}
}
}
输出:
文章参考:Java入门基础视频教程,java零基础自学就选黑马程序员Java入门教程(含Java项目和Java真题)