1.网络编程
有两种通信模型
C/S(Client/Server)基于客户端和服务器端,实现代码时候需要实现客户端与服务器端
B/S(Browser/Server)基于Http网页和服务器端,实现时候只需要实现服务器端即可
2.Echo模型(服务器与客户端实现通信)
所谓Echo模型就是指的是客户端发送消息到服务器端,服务器接收到消息然后将客户端接收到的信息进行回送到客户端。所以需要编写两个端口一个客户端,一个服务器端。java中可以使用Socket类实现用户端(客户端)与ServerSocket类(服务器端)
Socket与ServerSocket都是基于TCP(有连接的,可靠的传输服务)
Socket类(客户端)的常用方法:
方法 | 描述 |
public Socket(String host, int port) |
创建一个套接字,连接到指定主机名和端口号 |
public OutputStream getOutputStream() |
返回此套接字的输出流,用于向服务器发送数据,一般使用PrintStream |
public InputStream getInputStream() |
返回此套接字的输入流,用于从服务器接收数据 |
ServerSocket类(服务器端)的常用方法:
方法 | 描述 |
ServerSocket(int port) |
创建一个服务器套接字,绑定到指定的端口号 |
Socket accept() |
监听并接受客户端的连接请求,返回一个与客户端通信的新的 Socket 对象 |
void close() |
关闭服务器套接字,停止监听客户端连接请求 |
Echo模型实现案例代码:
1.服务器端代码实现:
package Example1903; import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; import java.util.Date; import java.util.Scanner; //服务器端 public class javaDemo { public static void main(String[] args) throws Exception{ Date date = new Date(); ServerSocket server = new ServerSocket(9999); System.out.println("等待客户端运行--------"); Socket client = server.accept(); // 设置服务器的输入输出流保证能接收信息与输入信息 Scanner scanner = new Scanner(client.getInputStream()); PrintStream out = new PrintStream(client.getOutputStream()); // 接收信息并且将信息传入out输出流 boolean flag = true; scanner.useDelimiter("\n"); while (flag){ if (scanner.hasNext()){ String value = scanner.next().trim(); if ("byebye".equalsIgnoreCase(value)){ out.println("ByeBye"); flag = false; }else out.println(date+ "[服务器]发送消息:"+value); } } // 关闭所有服务 server.close(); client.close(); scanner.close(); out.close(); } }
2.客户端代码实现:
package Example1904; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; import java.util.Date; import java.util.Scanner; //客户端 public class javaDemo { // 通过键盘输入信息 private static final BufferedReader KEYBOARD_INPUT = new BufferedReader(new InputStreamReader(System.in)); public static String getString(String promp) throws Exception{ System.out.print(promp); String str = KEYBOARD_INPUT.readLine(); return str; } public static void main(String[] args)throws Exception { Date date = new Date(); Socket client = new Socket("localhost",9999); // 获取同样的输入输出对象 Scanner scanner = new Scanner(client.getInputStream()); PrintStream out = new PrintStream(client.getOutputStream()); scanner.useDelimiter("\n");//使用分隔符 boolean flag = true; while (flag){ String input = getString( date+ "[客户端]请输入要发送的信息:").trim(); out.println(input);//将input内容放入PrintStream流中 if (scanner.hasNext()){ System.out.println(scanner.next()); } if ("byebye".equalsIgnoreCase(input)){ flag = false; } } } }
服务器端运行结果
客户端运行结果:
代码流程:
- 服务器端启动并等待连接:服务器启动后,通过
ServerSocket
监听指定端口(此处为9999),并调用accept
方法等待客户端连接。一旦有客户端连接成功,服务器会创建一个新的线程来处理与该客户端的通信。- 客户端连接服务器:客户端通过
Socket
构造函数连接到指定的服务器地址(此处为localhost
)和端口号(此处为9999
)。- 客户端发送消息:客户端通过
getString
方法获取用户输入的消息,并将消息通过PrintStream
的println
方法发送给服务器。- 服务器接收消息:服务器端在自己的线程中,通过
Scanner
的next
方法读取客户端发送的消息。- 服务器处理消息:服务器端根据接收到的消息进行相应的处理,可以是对消息进行解析、判断、计算等操作。在这个示例中,服务器端简单地将接收到的消息原样发送回客户端。
- 服务器返回消息:服务器通过
PrintStream
的println
方法将处理后的消息发送给客户端。- 客户端接收消息:客户端通过
Scanner
的next
方法读取服务器返回的消息,并将其打印到控制台。- 重复步骤3-7,直到客户端发送"byebye",表示结束通信。
答疑:
问1:两个代码是如何实现通信的 ?
首先两个进程之间的通信一定需要接收好对应的端口(计算机网络应用层知识)才能进行通信,然后两个代码共用了输入输出流(可以理解为两个人用了同一条电话线),客户端通过其输出流out将信息输出到服务器端,对于服务器外部输入就是输入流,即输出流变成了服务器的输入流,服务器通过scanner输入流接收客户端输出流的信息,同理服务器也通过其输出流输出到客户端,客户端接收到信息就输出
问2:scanner.useDelimiter("\n");useDelimiter方法知道是设置分隔符,但是搭配"\n"是有什么作用吗?
scanner.useDelimiter("\n");
设置了扫描器的分隔符为"\n",即换行符。这样做的作用是让扫描器以每一行作为一个输入元素,而不是以空白字符(默认情况下)作为分隔符。在这个示例中,服务器和客户端之间通过换行符来分隔消息的发送和接收。
问3:为什么要用到PrintStream正常的输出System.out.println代替不可以吗?
可以使用
System.out.println
来替代PrintStream
,它们都用于向控制台输出内容。示例代码中使用PrintStream
是为了将客户端发送给服务器的消息发送出去,以及将服务器返回的消息打印到客户端的控制台。
问4:服务器端用Scanner(socket.InputStream)是为了获取客户端数据这个可以理解,但是客户端为什么也是用Scanner(socket.InputStream)也是获取客户端?
在客户端和服务器端都使用
Scanner(socket.getInputStream())
是因为它们需要从对应的套接字(socket)的输入流中读取数据。客户端需要读取服务器发送过来的消息,而服务器需要读取客户端发送过来的消息。
问5:为什么要有private static final BufferedReader KEYBOARD_INPUT = new BufferedReader(new InputStreamReader(System.in));代码,不能直接通过Scanner(System.in)替代吗?
private static final BufferedReader KEYBOARD_INPUT = new BufferedReader(new InputStreamReader(System.in));
这段代码定义了一个静态的、只读的BufferedReader
类型的变量,通过System.in
将键盘输入流与该变量关联起来。这样做的目的是为了在客户端使用KEYBOARD_INPUT.readLine()
来获取用户输入的字符串。由于Scanner(System.in)
是使用缓冲区扫描输入,并不能满足逐行读取的需求,因此需要使用BufferedReader
来实现。
3.BIO处理模型(实现多用户访问同个服务器)
由于上一个模型里面一段时间只能有一个用户与服务器进行交互,并且一旦用户输入byebye服务器也就随之关闭,但是如果我想要有多个用户去与服务器交互那么该如何实现呢?所以引入了BIO处理模型。
如图结构可以知道,其解决方法是在ServeSocket内部创建多个Socket实例,并且将每一个Socket实例都封装在一个线程内,实现多线程通信,当有用户连接时候就创建一个独立的通信线程,每个用户可以独立关闭自己的线程
所以只需要修改服务器端的代码
服务器端:
package Example1907; import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; class EchoServer implements Runnable { // 初始化 Socket client = null; Scanner scanner = null; PrintStream out = null; boolean flag = true; // 传入通信对象实现创建输入输出流与确认客户端对象 public EchoServer(Socket client){ try { this.client = client; this.scanner = new Scanner(client.getInputStream()); this.out = new PrintStream(client.getOutputStream()); }catch (Exception e){ e.printStackTrace(); } } // 实现通信 @Override public void run() { while (this.flag){ String value = scanner.nextLine().trim(); if (value.equalsIgnoreCase("byebye")){ this.flag = false; out.println("Bye Bye~"); }else { out.println("[服务器端]:"+value); } } // 实现完通信则关闭所有服务 try { scanner.close(); out.close(); client.close(); }catch (Exception e){ e.printStackTrace(); } } } //服务器端 public class javaDemo { public static void main(String[] args) throws Exception { ServerSocket server = new ServerSocket(9999); boolean flag = true; System.out.println("等待客户端连接---------"); // 通过多线程接收多个客户端 while (flag){ Socket client = server.accept(); new Thread(new EchoServer(client)).start(); } } }
客户端:还是一样的(这里可以锻炼自己重写一遍代码)
package Example1908; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; import java.util.Scanner; //客户端 public class javaDemo { private static final BufferedReader KEYBOARD_INPUT = new BufferedReader(new InputStreamReader(System.in)); public static String getString(String promp)throws Exception{ System.out.print(promp); String input = KEYBOARD_INPUT.readLine().trim(); return input; } public static void main(String[] args) throws Exception{ Socket client = new Socket("localhost",9999); Scanner scanner = new Scanner(client.getInputStream()); PrintStream out = new PrintStream(client.getOutputStream()); scanner.useDelimiter("\n"); boolean flag = true; while (flag){ String msg = getString("[客户端]请输入你想要发送的消息").trim(); out.println(msg); if (scanner.hasNext()){ System.out.println(scanner.next().trim()); } if ("byebye".equalsIgnoreCase(msg)){ flag = false; } } scanner.close(); out.close(); client.close(); } }
效果展示:
服务器端:
以下javaDemo2是用户1,javaDemo4是用户2
客户端1:
客户端2:
这种模型是BIO模式,但是可以发现这里没有对线程数量的限制,也就意味着一旦用户数量急剧增加的时候就会出现性能大幅下降的情况所以需要追加对线程的限制, 也就是真正的BIO(Blocking Io 阻塞Io的模式)
4.UDP程序
UDP传输与Tcp不同的是其面向的是无连接的,不可靠的通信,如果Tcp是打电话,必须要知道对方号码建立连接才能进行通信,那么UDP就是在大街上随便喊一嗓子,有些人就听得到你说的话,也有些人离你太远听不到,但是你并不在乎。
这种传输方式常用于游戏之中,所谓的丢包就是因为udp并不可靠出现的问题,但是这种传输方式非常快延迟低。
UDP常用类有
DatagramPackage的常用方法:
方法名 | 描述 |
public DatagramPacket(byte[] buf, int length) |
构造一个 DatagramPacket 对象,使用指定的字节数组作为数据,指定的长度作为有效数据的长度。 |
public byte[] getData() | 获取该 DatagramPacket 对象中的数据字节数组。 |
public int getLength() | 获取该 DatagramPacket 对象中数据的长度。 |
DatagramSocket的常用方法:
方法名 |
描述 |
DatagramSocket() | 创建一个未绑定的数据报套接字。 |
DatagramSocket(int port) | 创建一个绑定到指定端口的数据报套接字。 |
DatagramSocket(int port, InetAddress address) | 创建一个绑定到指定端口和指定本地 IP 地址的数据报套接字。 |
void send(DatagramPacket packet) | 发送指定的数据包到目的地。 |
void receive(DatagramPacket packet) | 接收一个数据包,并将其存储在指定的数据包对象中 |
以下案例实现一个客户端通过udp发送报文到服务器并输出服务器返回值
在上一个服务器开着的前提下:
package Example1910; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.nio.charset.StandardCharsets; public class javaDemo { public static void main(String[] args)throws Exception { // 通过键盘输入数据 System.out.println("请输入你想要发送的数据"); BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); String data = input.readLine(); // 发送数据包 DatagramSocket client = new DatagramSocket(9999); DatagramPacket packet = new DatagramPacket(data.getBytes(StandardCharsets.UTF_8),0,data.length(), InetAddress.getByName("localhost"),9999); client.send(packet); // 接收数据包 byte redata[] = new byte[1024]; DatagramPacket repacket = new DatagramPacket(redata,0,redata.length); client.receive(repacket); System.out.println("接收的数据是"+new String(redata,0,redata.length,StandardCharsets.UTF_8)); client.close(); } }
编辑