网络编程:可以让设备中的程序与网络上其他设备中的程序进行数据交互的技术(实现网络通信)
- 基本的通信架构有两种:CS架构(Client客户端/Server服务端)、BS架构(Browser浏览器/Server服务端)
- 无论是哪种架构都需要依赖于网络编程
1. 网络编程三要素
1.1 IP
设备在网络中的地址,是设备在网络中的唯一标识
目前,被广泛采用的IP地址形式有两种:IPv4,IPv6
- IPv4使用32位地址,通常以点分十进制表示
- IPv6使用128位地址,分为8段,每段每四位编码成一个十六进制表示,每段之间用:分开,这种方式称为冒分十六进制
IP域名:用于在互联网上识别和定位网站的人类可读的名称
eg:www.baidu.com
- **DNS域名解析:**是互联网中用于将域名转换为对应IP地址的分布式命名系统,将容易记住的域名映射到数字化的IP地址,使得用户可以通过域名来访问网站和其他网络资源
公网IP:可以连接到互联网的IP地址
内网IP:局域网IP,是只能组织机构内部使用的IP地址,专门为组织机构内部使用
本机IP:127.0.0.1/localhost:代表本机IP,只会寻找当前程序所在的主机
IP常用命令
- ipconfig:查看本机IP地址,ipconfig/all 查看本机物理Ip
- ping IP地址:检查网络是否连通
InetAddress:代表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) | 根据ip地址或域名,返回一个InetAddress对象 |
public boolean isReachable(int timeout)throw IOException | 判断主机在指定毫秒内与该ip对应的主机是否能连通 |
package com.kun.demo1InetAddress;
import java.net.InetAddress;
public class InetAddressDemo1 {
public static void main(String[] args) {
try {
// 获取本机Ip对象
InetAddress localhost= InetAddress.getLocalHost();
System.out.println(localhost.getHostName());
System.out.println(localhost.getHostAddress());
// 获取指定IP对象--域名
InetAddress inet1 = InetAddress.getByName("www.baidu.com");
System.out.println(inet1.getHostName());
System.out.println(inet1.getHostAddress());
// 3. 判断本机和对方主机是否互通,查询5s内本机是否可以与百度接通
System.out.println(inet1.isReachable(5000)); // true
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.2 端口
应用程序在设备中的唯一标识,用来标记正在计算机设备上运行的应用程序,被规定为一个16进制的二进制,范围是0-2的16次方-1
端口分类:
- 周知端口:0-1023,被预先定义的知名应用占用
- 注册端口:1024-49151,分配给用户进程或某些应用程序
- 动态端口:49152- 一般不固定分配某种进程,而是动态分配
1.3 协议
连接和数据在网络中传输的规则,网络上通信的设备,事先规定的连接规则,以及传输数据的规则
开放式网络互联标准:osi网络参考模型
- OSI网络参考模型:全球网络互联标准
- TCP/IP网络模型:事实上的国际标准
传输层的两个通信协议
- UDP:用户数据报协议
- TCP:传输控制协议
2. UDP通信
**特点:**无连接、不可靠通信、但是通信效率高
不事先建立连接,数据按照包发送,一包数据包含:自己的IP,端口,目的地IP,端口和数据(限制在64KB以内)等
发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,所以是不可靠的
java提供了一个java.net.DatagramSocket类实现UDP通信
2.1 创建和方法
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) | 创建用来接收数据的数据包 |
2.2 UPD通信入门
启动程序时一定要先启动服务端再启动客户端,否则客户端找不到port发送数据
创建客户端
package com.kun.udp1;
import java.io.IOException;
import java.net.*;
public class UDPClientDemo1 {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动启动启动!!!!!");
// 1、创建发送端对象
DatagramSocket socket = new DatagramSocket();
// 2. 创建数据包对象封装需要发送的数据
byte[] bytes = "nibukunwokun".getBytes();
/**
* byte[] buf; 发送的字节数据
* int length; 需要发送的长度
* InetAddress address; 目标的IP地址 这里做测试,目标地址也是本机
* int port; 目标的服务端口
*/
DatagramPacket packet = new DatagramPacket(bytes,bytes.length, InetAddress.getLocalHost(),8080);
// 3. 让发送方发送数据包的数据
socket.send(packet);
}
}
创建服务端:
package com.kun.udp1;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServerDemo2 {
public static void main(String[] args) throws IOException {
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);
System.out.println("客户端的端口号是:"+port); // 客户端的ip是:172.19.9.191
//客户端的端口号是:64095 这里的端口是随机分配的端口,为什么随机,因为客户端是发送数据,不需要指定端口,而服务端需要找到然后向其发送数据,所以需要指定端口
}
}
2.3 多发多收
客户端实现步骤
- 创建DatagramSocket对象(发送端对象)
- 使用while死循环不断的接收用户的数据输入,如果用户输入的exit则退出程序
- 如果用户输入的不是exit,则把数据封装成DatagramPacket
- 使用DatagramSocket对象的send方法将数据包对象进行发送
- 释放资源
package com.kun.udp2;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;
public class UDPClientDemo1 {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动启动启动!!!!!");
// 1、创建发送端对象
DatagramSocket socket = new DatagramSocket();
Scanner sc = new Scanner(System.in);
while (true) {
// 2. 创建数据包对象封装需要发送的数据
System.out.println("请说");
String msg = sc.nextLine(); // nextLine可以把一行数据全收走
// 3. 如果用户输入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);
// 4. 让发送方发送数据包的数据
socket.send(packet);
}
}
}
服务端实现步骤
- 创建DatgramPacket对象并指定端口(接收端对象)
- 创建DatagramPacket对象接收数据(数据包对象)
- 使用DatagramPacket对象的receive方法传入DatagramPacket对象
- 使用while死循环不断进行第三步
package com.kun.udp2;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServerDemo2 {
public static void main(String[] args) throws IOException {
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("---------------------");
}
}
}
UDP的接收端为什么可以接收很多发送端的消息
- 接收端只负责接收数据包,无所谓那个发送端的数据包
3. TCP通信
**特点:**面向连接,可靠通信,通信效率相对不高
保证在不可靠的信道上实现可靠的数据传输
TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接
3.1 握手和挥手
TCP三次握手
- 客户端向服务端发送SYN包,表示请求连接
- 服务端接收后会回应一个SYN-ACK包,表示同意连接
- 客服端接收后会回应一个ACK包,表示确认连接已经建立
连接为啥要3次握手
- 防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。
- 三次握手可以确保客户端和服务端都能够正常发送和接收数据。
TCP四次挥手
- 主动关闭方发送FIN包,表示希望断开连接
- 被动关闭方收到FIN包后,会发送一个ACK包表示已经确认了对方的关闭请求
- 被动关闭方完成了所有的工作并准备关闭连接的时候会向对方发送FIN包
- 主动关闭方收到FIN包后会发送最后一个ACK包确认,然后进入到TIME-WAIT状态,状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入CLOSED 状态。
为啥要四次挥手
由于TCP连接是双向的,每一端都需要独立地通知另一端它已完成数据传输并希望关闭连接。
确保了连接能够在双方都准备好结束通信的情况下安全、有序地关闭。
3.2 入门
客户端开发
java提供了一个java.net.Socket类实现TCP通信
客户端程序通过java.net包下的Socket类来实现的
构造器 | 说明 |
---|---|
public Socket(String host, int port) | 根据指定的服务器ip,端口号请求与服务端建立连接,连接通过,就获取了客户端的socket |
TCP通信实现一发一收,客户端开发
- 创建客户端的Socket对象,请求与服务端的连接
- 使用socket对象调用getOutputStream()方法得到字节输出流
- 使用字节输出流完成数据的发送
- 释放资源:关闭socket管道
package com.kun.tcp1;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
public class ClientDemo1 {
public static void main(String[] args) throws Exception {
// 1. 创建Socket对象,请求与服务端的Socket连接
Socket socket = new Socket("172.19.9.***",9999);
// 2. 从Socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3. 将字节输出流封装为一个特殊数据流
DataOutputStream dos = new DataOutputStream(os);
dos.writeInt(1);
dos.writeUTF("nibukunwokun");
// 4. 释放管道
// socket.close();
}
}
服务端开发
- 服务端开发通过java.net包下的ServerSocket类来实现的
构造器 | 说明 |
---|---|
public ServerSocket(int port) | 为服务端程序注册端口 |
方法
方法 | 说明 |
---|---|
public Socket accept() | 阻塞等待客户端的连接请求,一旦与某个客户端成功连接,则返回服务端这边的Socket对象 |
服务端开发步骤
- 创建ServerSocket对象,注册服务端端口
- 调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象
- 通过Socket对象调用getInputStream()方法得到字节输入流,完成数据的接收
- 释放资源,但是一般来说,服务端不需要关闭socket管道
package com.kun.tcp1;
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 {
System.out.println("服务端启动啦---------------------------");
// 一发一收,服务端开发
// 1、创建服务端的ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket sc = new ServerSocket(9999);
// 2.调用accept方法,阻塞式等待客户端连接,一旦有客户端连接,会返回一个Socket对象,代表服务端管道
Socket socket = sc.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()+
"客户端端口:"+socket.getPort());
}
}
3.3 多发多收
客户端使用循环反复地发送消息
package com.kun.tcp2;
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 {
System.out.println("客户端启动啦------------------------------");
// 1. 创建Socket对象,请求与服务端的Socket连接
Socket socket = new Socket("172.19.9.191",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(); // 关闭通信管道
break;
}
dos.writeUTF(msg);
dos.flush(); // 刷新数据
}
// 4. 释放管道
// socket.close();
}
}
服务端
package com.kun.tcp2;
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 {
System.out.println("服务端启动啦---------------------------");
// 多发多收,服务端开发
// 1、创建服务端的ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket sc = new ServerSocket(9999);
// 2.调用accept方法,阻塞式等待客户端连接,一旦有客户端连接,会返回一个Socket对象,代表服务端管道
Socket socket = sc.accept();
// 3. 获取输入流,得到客户端发送来的数据
InputStream is = socket.getInputStream();
// 4. 把字节输入流包装成特殊数据输入流(和服务端对应)
DataInputStream dis = new DataInputStream(is);
// 5. 读取数据,必须和客户端一致
//int id = dis.readInt();
while (true) {
// 读取数据
String msg = dis.readUTF();
System.out.println("收到的客户端msg是:" + msg);
// 6. 得到客户端ip和端口
System.out.println("客户端ip:"+socket.getInetAddress().getHostAddress()+
"客户端端口:"+socket.getPort());
System.out.println("------------------------");
}
}
}
3.4 同时接收多个客户端的消息
主线程:负责接收客户端连接,定义了循环接收客户端Socket管道连接
package com.kun.demo6tcp3;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo2 {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动啦---------------------------");
// 多发多收,服务端开发
// 1、创建服务端的ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket sc = new ServerSocket(9999);
while (true) {
// 2.调用accept方法,阻塞式等待客户端连接,一旦有客户端连接,会返回一个Socket对象,代表服务端管道
Socket socket = sc.accept();
// 追踪上线
System.out.println("一个客户端上线了:"+socket.getInetAddress().getHostAddress());
// 3. 把这个客户端管道交给一个子线程专门负责接收这个管道的消息
new ServerReader(socket).start();
}
}
}
子线程:单独处理socket,每接收一个Socket通信管道后就分配一个独立的线程负责处理它
线程类
package com.kun.demo6tcp3;
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 {
// 负责读管道的消息字节输入流
InputStream is = socket.getInputStream();
// 封装成数据输入流
DataInputStream dis = new DataInputStream(is);
while (true) {
String msg = dis.readUTF();
System.out.println(msg);
System.out.println("客户端的Ip:"+ socket.getLocalAddress().getHostAddress());
System.out.println("端口号:"+socket.getPort());
}
} catch (Exception e) {
e.printStackTrace();
// 追踪客户端下线
System.out.println("客户端" + socket.getInetAddress().getHostAddress() + "下线啦");
}
}
}
3.5 B/S架构的原理
服务器必须给浏览器响应HTTP协议规定的数据格式,否则浏览器不识别返回的数据
package com.kun.demo7tcp4;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动啦---------------------------");
// 1、创建服务端的ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket sc = new ServerSocket(8080);
while (true) {
// 2.调用accept方法,阻塞式等待客户端连接,一旦有客户端连接,会返回一个Socket对象,代表服务端管道
Socket socket = sc.accept();
// 追踪上线
System.out.println("一个客户端上线了:"+socket.getInetAddress().getHostAddress());
// 3. 把这个客户端管道交给一个子线程专门负责接收这个管道的消息
new ServerReader(socket).start();
}
}
}
线程类
package com.kun.demo7tcp4;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
public class ServerReader extends Thread{
private Socket socket;
public ServerReader(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("<title>");
ps.println("服务器响应数据");
ps.println("</title>");
ps.println("</head>");
ps.println("<body>");
ps.println("<h1>瓦达西是胡图图</h1>");
ps.println("</body>");
ps.println("</html>");
ps.close();
// 浏览器请求是短连接
socket.close();
} catch (Exception e) {
e.printStackTrace();
System.out.println("客户端" + socket.getInetAddress().getHostAddress() + "下线啦");
}
}
}
使用线程池进行优化,进行线程复用
package com.kun.demo7tcp4;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
public class ServerDemo {
public static void main(String[] args) throws Exception {
System.out.println("服务端启动啦---------------------------");
// 1、创建服务端的ServerSocket对象,绑定端口号,监听客户端连接
ServerSocket sc = new ServerSocket(8080);
// 创建线程池,3个核心线程,最大线程数10个,队列里放100个
ExecutorService pool = new ThreadPoolExecutor(3,10,10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
while (true) {
// 2.调用accept方法,阻塞式等待客户端连接,一旦有客户端连接,会返回一个Socket对象,代表服务端管道
Socket socket = sc.accept();
// 追踪上线
System.out.println("一个客户端上线了:"+socket.getInetAddress().getHostAddress());
// 3. 把客户端请求包装成一个线程任务交给线程池处理(Thread线程对象可以作为一个线程任务对象使用)
pool.execute(new ServerReaderRunnable(socket));
}
}
}