IO模型-基于BIO的Java网络编程

InetAddress

一个 InetAddress 类的对象就代表一个 IP 地址对象

成员方法:

  • static InetAddress getLocalHost():获得本地主机 IP 地址对象
  • static InetAddress getByName(String host):根据 IP 地址字符串或主机名获得对应的 IP 地址对象
  • String getHostName():获取主机名
  • String getHostAddress():获得 IP 地址字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class InetAddressDemo {
public static void main(String[] args) throws Exception {
// 1.获取本机地址对象
InetAddress ip = InetAddress.getLocalHost();
System.out.println(ip.getHostName());//DESKTOP-NNMBHQR
System.out.println(ip.getHostAddress());//192.168.11.1
// 2.获取域名ip对象
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());//www.baidu.com
System.out.println(ip2.getHostAddress());//14.215.177.38
// 3.获取公网IP对象。
InetAddress ip3 = InetAddress.getByName("182.61.200.6");
System.out.println(ip3.getHostName());//182.61.200.6
System.out.println(ip3.getHostAddress());//182.61.200.6

// 4.判断是否能通: ping 5s之前测试是否可通
System.out.println(ip2.isReachable(5000)); // ping百度
}
}

UDP

基本介绍

UDP(User Datagram Protocol)协议的特点:

  • 面向无连接的协议,发送端只管发送,不确认对方是否能收到,速度快,但是不可靠,会丢失数据
  • 尽最大努力交付,没有拥塞控制
  • 基于数据包进行数据传输,发送数据的包的大小限制 64KB 以内
  • 支持一对一、一对多、多对一、多对多的交互通信

UDP 协议的使用场景:在线视频、网络语音、电话

实现UDP

UDP 协议相关的两个类:

  • DatagramPacket(数据包对象):用来封装要发送或要接收的数据,比如:集装箱
  • DatagramSocket(发送对象):用来发送或接收数据包,比如:码头

DatagramPacket

  • DatagramPacket 类:

    public new DatagramPacket(byte[] buf, int length, InetAddress address, int port):创建发送端数据包对象

    • buf:要发送的内容,字节数组
    • length:要发送内容的长度,单位是字节
    • address:接收端的IP地址对象
    • port:接收端的端口号

    public new DatagramPacket(byte[] buf, int length):创建接收端的数据包对象

    • buf:用来存储接收到内容
    • length:能够接收内容的长度
  • DatagramPacket 类常用方法:

    • public int getLength():获得实际接收到的字节个数
    • public byte[] getData():返回数据缓冲区

DatagramSocket

  • DatagramSocket 类构造方法:
    • protected DatagramSocket():创建发送端的 Socket 对象,系统会随机分配一个端口号
    • protected DatagramSocket(int port):创建接收端的 Socket 对象并指定端口号
  • DatagramSocket 类成员方法:
    • public void send(DatagramPacket dp):发送数据包
    • public void receive(DatagramPacket p):接收数据包
    • public void close():关闭数据报套接字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class UDPClientDemo {
public static void main(String[] args) throws Exception {
System.out.println("===启动客户端===");
// 1.创建一个集装箱对象,用于封装需要发送的数据包!
byte[] buffer = "我学Java".getBytes();
DatagramPacket packet = new DatagramPacket(buffer,bubffer.length,InetAddress.getLoclHost,8000);
// 2.创建一个码头对象
DatagramSocket socket = new DatagramSocket();
// 3.开始发送数据包对象
socket.send(packet);
socket.close();
}
}
public class UDPServerDemo{
public static void main(String[] args) throws Exception {
System.out.println("==启动服务端程序==");
// 1.创建一个接收客户都端的数据包对象(集装箱)
byte[] buffer = new byte[1024*64];
DatagramPacket packet = new DatagramPacket(buffer, bubffer.length);
// 2.创建一个接收端的码头对象
DatagramSocket socket = new DatagramSocket(8000);
// 3.开始接收
socket.receive(packet);
// 4.从集装箱中获取本次读取的数据量
int len = packet.getLength();
// 5.输出数据
// String rs = new String(socket.getData(), 0, len)
String rs = new String(buffer , 0 , len);
System.out.println(rs);
// 6.服务端还可以获取发来信息的客户端的IP和端口。
String ip = packet.getAddress().getHostAdress();
int port = packet.getPort();
socket.close();
}
}

TCP

基本介绍

TCP/IP (Transfer Control Protocol) 协议,传输控制协议

TCP/IP 协议的特点:

  • 面向连接的协议,提供可靠交互,速度慢
  • 点对点的全双工通信
  • 通过三次握手建立连接,连接成功形成数据传输通道;通过四次挥手断开连接
  • 基于字节流进行数据传输,传输数据大小没有限制

TCP 协议的使用场景:文件上传和下载、邮件发送和接收、远程登录

注意:TCP 不会为没有数据的 ACK 超时重传

三次握手 四次挥手

推荐阅读:https://yuanrengu.com/2020/77eef79f.html

Socket

TCP 通信也叫 Socket 网络编程,只要代码基于 Socket 开发,底层就是基于了可靠传输的 TCP 通信

双向通信:Java Socket 是全双工的,在任意时刻,线路上存在 A -> BB -> A 的双向信号传输,即使是阻塞 IO,读和写也是可以同时进行的,只要分别采用读线程和写线程即可,读不会阻塞写、写也不会阻塞读

TCP 协议相关的类:

  • Socket:一个该类的对象就代表一个客户端程序。
  • ServerSocket:一个该类的对象就代表一个服务器端程序。

Socket 类:

  • 构造方法:

    • Socket(InetAddress address,int port):创建流套接字并将其连接到指定 IP 指定端口号

    • Socket(String host, int port):根据 IP 地址字符串和端口号创建客户端 Socket 对象

      注意事项:执行该方法,就会立即连接指定的服务器,连接成功,则表示三次握手通过,反之抛出异常

  • 常用 API:

    • OutputStream getOutputStream():获得字节输出流对象
    • InputStream getInputStream():获得字节输入流对象
    • void shutdownInput():停止接受
    • void shutdownOutput():停止发送数据,终止通信
    • SocketAddress getRemoteSocketAddress():返回套接字连接到的端点的地址,未连接返回 null

ServerSocket 类:

  • 构造方法:public ServerSocket(int port)

  • 常用 API:public Socket accept()阻塞等待接收一个客户端的 Socket 管道连接请求,连接成功返回一个 Socket 对象

    三次握手后 TCP 连接建立成功,服务器内核会把连接从 SYN 半连接队列(一次握手时在服务端建立的队列)中移出,移入 accept 全连接队列,等待进程调用 accept 函数时把连接取出。如果进程不能及时调用 accept 函数,就会造成 accept 队列溢出,最终导致建立好的 TCP 连接被丢弃

相当于客户端和服务器建立一个数据管道(虚连接,不是真正的物理连接),管道一般不用 close

实现TCP

开发流程

客户端的开发流程:

  1. 客户端要请求于服务端的 Socket 管道连接
  2. 从 Socket 通信管道中得到一个字节输出流
  3. 通过字节输出流给服务端写出数据

服务端的开发流程:

  1. 用 ServerSocket 注册端口
  2. 接收客户端的 Socket 管道连接
  3. 从 Socket 通信管道中得到一个字节输入流
  4. 从字节输入流中读取客户端发来的数据

  • 如果输出缓冲区空间不够存放主机发送的数据,则会被阻塞,输入缓冲区同理
  • 缓冲区不属于应用程序,属于内核
  • TCP 从输出缓冲区读取数据会加锁阻塞线程
实现通信

需求一:客户端发送一行数据,服务端接收一行数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ClientDemo {
public static void main(String[] args) throws Exception {
// 1.客户端要请求于服务端的socket管道连接。
Socket socket = new Socket("127.0.0.1", 8080);
// 2.从socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.把低级的字节输出流包装成高级的打印流。
PrintStream ps = new PrintStream(os);
// 4.开始发消息出去
ps.println("我是客户端");
ps.flush();//一般不关闭IO流
System.out.println("客户端发送完毕~~~~");
}
}
public class ServerDemo{
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1.注册端口: public ServerSocket(int port)
ServerSocket serverSocket = new ServerSocket(8080);
// 2.开始等待接收客户端的Socket管道连接。
Socket socket = serverSocket.accept();
// 3.从socket通信管道中得到一个字节输入流。
InputStream is = socket.getInputStream();
// 4.把字节输入流转换成字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 6.按照行读取消息 。
String line;
if((line = br.readLine()) != null){
System.out.println(line);
}
}
}

需求二:客户端可以反复发送数据,服务端可以反复数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ClientDemo {
public static void main(String[] args) throws Exception {
// 1.客户端要请求于服务端的socket管道连接。
Socket socket = new Socket("127.0.0.1",8080);
// 2.从socket通信管道中得到一个字节输出流
OutputStream os = socket.getOutputStream();
// 3.把低级的字节输出流包装成高级的打印流。
PrintStream ps = new PrintStream(os);
// 4.开始发消息出去
while(true){
Scanner sc = new Scanner(System.in);
System.out.print("请说:");
ps.println(sc.nextLine());
ps.flush();
}
}
}
public class ServerDemo{
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1.注册端口: public ServerSocket(int port)
ServerSocket serverSocket = new ServerSocket(8080);
// 2.开始等待接收客户端的Socket管道连接。
Socket socket = serverSocket.accept();
// 3.从socket通信管道中得到一个字节输入流。
InputStream is = socket.getInputStream();
// 4.把字节输入流转换成字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 6.按照行读取消息 。
String line;
while((line = br.readLine()) != null){
System.out.println(line);
}
}
}

需求三:实现一个服务端可以同时接收多个客户端的消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ClientDemo {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1",8080);
OutputStream os = new socket.getOutputStream();
PrintStream ps = new PrintStream(os);
while(true){
Scanner sc = new Scanner(System.in);
System.out.print("请说:");
ps.println(sc.nextLine());
ps.flush();
}
}
}
public class ServerDemo{
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
ServerSocket serverSocket = new ServerSocket(8080);
while(true){
// 开始等待接收客户端的Socket管道连接。
Socket socket = serverSocket.accept();
// 每接收到一个客户端必须为这个客户端管道分配一个独立的线程来处理与之通信。
new ServerReaderThread(socket).start();
}
}
}
class ServerReaderThread extends Thread{
privat Socket socket;
public ServerReaderThread(Socket socket){this.socket = socket;}
@Override
public void run() {
try(InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is))
){
String line;
while((line = br.readLine()) != null){
sout(socket.getRemoteSocketAddress() + ":" + line);
}
}catch(Exception e){
sout(socket.getRemoteSocketAddress() + "下线了~~~~~~");
}
}
}

上述缺点:

  • 一个客户端对应一个线程,容易导致资源耗尽
  • 在客户端接入但并未发送消息的情况下,线程空等,浪费资源

伪异步

伪异步

一个客户端要一个线程,并发越高系统瘫痪的越快,可以在服务端引入线程池,使用线程池来处理与客户端的消息通信

  • 优势:不会引起系统的死机,可以控制并发线程的数量

  • 劣势:同时可以并发的线程将受到限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class BIOServer {
public static void main(String[] args) throws Exception {
//线程池机制
//创建一个线程池,如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true) {
System.out.println("线程名字 = " + Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接....");
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//创建一个线程,与之通讯
newCachedThreadPool.execute(new Runnable() {
public void run() {
//可以和客户端通讯
handler(socket);
}
});
}
}

//编写一个handler方法,和客户端通讯
public static void handler(Socket socket) {
try {
System.out.println("线程名字 = " + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
//通过socket获取输入流
InputStream inputStream = socket.getInputStream();
int len;
//循环的读取客户端发送的数据
while ((len = inputStream.read(bytes)) != -1) {
System.out.println("线程名字 = " + Thread.currentThread().getName());
//输出客户端发送的数据
System.out.println(new String(bytes, 0, read));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("关闭和client的连接");
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
文章作者: GeYu
文章链接: https://nuistgy.github.io/2023/06/26/IO-传统BIO下网络编程/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Yu's Blog