网络发展 略
局域网/广域网
操作系统中的‘文件’是一个广义概念
平时说的文件,只是指普通文件
实际上,操作系统中的文件还可能表示了一些硬件设备/软件资源
socket 文件,就对应这‘网卡’这种硬件设备
从socket文件读数据,本质上就是读网卡
往socket文件写数据,本质上就是写网卡
最简单的客户端服务器程序,回显服务-EchoServer
这样的程序不涉及任何业务逻辑,就只是通过socket api 单纯的转发
socket = new DatagramSocket(port)
此处在构造服务器这边的socket对象的时候,就需要显示的绑定一个端口号
端口号是用来区分一个应用程序的~主机收到网卡上数据的时候,这个数据该给哪个程序?
构造socket对象有很多失败的可能
1.端口号已经被占用了,两个人不能有相同的电话号码,同一个主机的两个进程也不能有相同的端口号
2.每个进程能偶打开的文件个数,是有上限的,如果进程之前已经打开了很多很多的文件,就可能导致此处的socket文件不能顺利打开。
第一步
package network;
import javax.sql.DataSource;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
//进行网络编程,第一步就需要先准备好socket实例~这是进行网络编程的大前提
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
}
第二步
启动服务器
//1.读取客户端发来的请求 //2.根据请求计算响应(由于咱们这是一个回显服务) //3. 把响应写回到客户端
为啥服务器上来就是接受,而不是发送呢
因为服务器的定义:就是‘被动接受请求’的一方
主动发送请求的这一方,叫做客户端
send方法参数,也是DatagramPacket 需要把响应数据先构造成一个DatagramPacket 再进行发送
这里不是构造一个空的数据报
这里的参数不再是一个空的字节数组了,response是刚才根据请求计算得到的响应,非空的
DatagramPacket里面的数据就是String response的数据
这里拿到的是字节数组的长度(字节的个数)
改进之后的版本在DatagramPacket 构造方法中,指定了第三个参数,表示要把数据发给哪个 地址 + 端口 socketaddres可视为一个类,里面包含了IP和端口 DatagramPacket reponsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
一 服务器完整代码
package network;
import javax.sql.DataSource;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
// 站在服务器的角度
// 1.源IP:服务器绑定的端口(此处手动指定 9090)
// 2.源端口:服务器绑定的端口(此处指定了 9090)
// 3.目的IP:包含在收到的数据报中(客户端IP)
// 4.目的端口:包含在收到的数据包中
// 5.协议类型UDP
public class UdpEchoServer {
//进行网络编程,第一步就需要先准备好socket实例~这是进行网络编程的大前提
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
// 启动服务器
public void start() throws IOException {
System.out.println("启动服务器");
//UDP不需要建立连接,直接接受从客户端来的数据即可
while ((true)){
//1.读取客户端发来的请求
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);//为了接受数据,需要先准备好一个空的 DatagramPacket 对象,由receive 来进行填充数据
// 把DatagramPacket 解析成一个String
String request = new String(requestPacket.getData(),0,requestPacket.getLength(),"UTF-8");
//2.根据请求计算响应(由于咱们这是一个回显服务)
String response = process(request);
//3. 把响应写回到客户端
DatagramPacket reponsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
socket.send(reponsePacket);
System.out.printf("[%s:%d] req: %s, resp: %s\n",
requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
}
}
//由于是回显服务,响应就和请求一样
//实际上对于一个真实的服务器来说,这个过程是最复杂的,为了实现这个过程需要几万甚至几十万行代码
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
二
构造客户端
在客户端构造socket对象的时候,就不再手动指定端口号,使用无参版本的构造方法
不指定端口号,让操作系统自己分配一个空闲的端口号
通常手写代码的时候,服务器都是手动指定的,客户端都是由系统自动指定的
对于服务器来说,必须手动指定~后续客户端要根据这个端口来访问到服务器~
如果让系统随机分配,客户端就不知道服务器的端口是啥,不能访问。
对于客户端来说,如果手动指定,也行,但是系统随机分配更好
一个及其上的两个进程不能绑定同一个端口
客户端就是普通用户的电脑,程序复杂不,对应端口麻烦
万一被占用,就无法正常工作了。
而且由于客户端是主动发起请求的一方,客户端需要在发送请求之前先知道服务器的地址+端口
但是反过来在请求出去之前,服务器是不需要事先知道客户端的地址+端口
这种写法,也是,既构造了数据,有能构造目标地址,这个目标弟子,IP端口是合在一起的写法
写代码的时候,就会涉及到一系列的ip和端口
”五元组“
一次通信,是有五个核心信息,描述出来的
源IP,源端口,目的IP,目的端口,协议类型
完整代码
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
// 站在客户端的角度:
// 源 IP:本机IP
// 源端口:系统分配的端口
// 目的 IP:服务器的IP
// 目的端口:服务器的端口
// 协议类型UDP
/**
* Created with IntelliJ IDEA.
* Description:
* User: 星有野
* Date: 2022-07-12
* Time: 20:56
*/
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
public UdpEchoClient(String ip, int port) throws SocketException {
//此处的 port 是服务器的端口
//客户端启动的时候,不需要给socket指定端口,客户端自己的端口是系统随机分配的
socket = new DatagramSocket();
serverIP = ip;
serverPort = port;
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true){
//1. 先把控制台读取用户输入的字符
System.out.printf(" -> ");
String request = scanner.next();
//2. 把这个用户输入的内容,构造一个UDP请求,并发送
// 构造的请求里包含两部分信息
// 1)数据的内容 request 字符串
// 2)数据要发给谁~ 服务器的 IP + 端口号
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName("127.0.0.1"),9090 );
socket.send(requestPacket);
//3. 从服务器读取响应数据报,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"UTF-8");
//4. 把响应结果显示到控制台上
System.out.printf("req: %s, resp: %s\n", request, response);
}
}
public static void main(String[] args) throws IOException {
//由于服务器和客户端在同一个机器上,使用的IP仍然是127.0.0.1,如果是在不同的机器上,当然就需要改这里的IP
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
客户端可以有很多,一个服务器可以给很多很多客户端提供服务,一个餐馆可以给很多的客人提供就餐服务
能处理多少客户端取决于
1.处理一个请求消耗多少资源
2.机器一共有多少资源
当想再启动一个时会提示将之前的关掉
此时就需要进行一下操作
写一个翻译程序
请求是一些简单的英文单词~响应也是英文单词对应的翻译
客户端不变,把服务器代码进行调整
主要是调整process方法
读取请求并解析,把响应写回给客户端,这俩步骤都一样
关键的逻辑就是 根据请求处理响应
private不可被重写需要改成public
public class UdpDictServer extends UdpEchoServer
package network;
import java.net.SocketException;
public class UdpDictServer extends UdpEchoServer{
public UdpDictServer(int port) throws SocketException {
super(port);
}
@Override
public String process(String request) {
}
}
把process重写即可
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer{
private HashMap<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
//提前存好
//简单够早几个词
dict.put("最美的人","皮皮");
dict.put("最棒的摄影师","皮皮");
dict.put("最可爱的人","皮皮");
dict.put("最好的女朋友","皮皮");
dict.put("皮皮的相机是","EOSRP");
}
//多态
@Override
public String process(String request) {
return dict.getOrDefault(request,"该词无法被翻译");
}
public static void main(String[] args) throws IOException {
UdpDictServer server = new UdpDictServer(9090);
server.start();
}
}
客户端不变,把服务器代码进行调整
主要是调整process方法
读取请求并解析,把响应写回给客户,这俩步骤都一样
关键的逻辑是”请求处理响应“
学习下TCP版本的客户端服务器的代码
TCP api中,也是涉及到两个核心的类
ServerSocket 专门给TCP服务器用的
Socket(既需要给服务器用,又需要给客户端用)
服务机完整代码
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
// listen => 英文原意 监听
// 但是再 Java socket 中是体现不出来 监听的含义的
// 之所以这么叫,其实是 操作系统原生的API 里有一个操作叫做listen
// private ServerSockrt listenSocket = null;
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
//由于TCP 是由连接的,不能一上来就读数据,而要先建立链接(接电话)
// accept 就是在”接电话“,接电话的前提是,有人给你打了,如果当前没有客户端尝试建立连接,此处的accept就会阻塞
// accept返回了一个socket对象,称为clientSocket,后续和客户之间的沟通,都是通过clientSocket来完成的
//进一步讲 serverSocket 就干了一件事 接电话
Socket clientSocket = serverSocket.accept();
// 改进方法 在这个地方,每次accept 成功,都创建一个新的线程,由新线程负责执行这个 processConnection 方法
Thread t = new Thread(() -> {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
t.start();
processConnection(clientSocket);
}
}
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端按建立连接!",clientSocket.getInetAddress().toString(),clientSocket.getPort());
//接下来处理请求和响应
//这里的针对 TCP socket 的读写就和文件读写是一摸一样的!!
try(InputStream inputStream = clientSocket.getInputStream()){
try(OutputStream outputStream = clientSocket.getOutputStream()){
//循环的处理每个请求,分别返回响应
Scanner scanner = new Scanner(inputStream);
while (true){
//读取请求
if(!scanner.hasNext()){
System.out.printf("[%s:%d] 客户端按建立连接!",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
// 此处用 scanner 更方便,如果不用Scanner就用原生的
String request = scanner.next();
// 2.根据请求,计算响应
String response = process(request);
//3.把这个响应返回给客户端
//为了方便期间,可以使用PrintWriter 把 OutputStream
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
//刷新缓冲区
printWriter.flush();
System.out.printf("[%s:%d] 客户端按建立连接!",
clientSocket.getInetAddress().toString(),clientSocket.getPort());
}
}
}catch (IOException e){
e.printStackTrace();
}finally {
//此处记得关闭操作
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
客户机完整代码
package network;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.io.IOException;
import java.util.Scanner;
public class TcpEchoClient {
// 用普通的 socket 即可,不用ServerSocket了
// 此处也不用动手给客户端指定端口号,让系统自由分配
private Socket socket = null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException{
// 其实这里是可以给你的,但是这里给了以后,含义是不同的
// 这里传入的 ip 和端口号 的含义表示的不是自己绑定,而是表示和这个ip端口建立连接
// 调用这个构造方法,就会和服务器建立连接
socket = new Socket(serverIP, serverPort);
}
public void start(){
System.out.println("和服务器连接成功");
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream()){
try (OutputStream outputStream = socket.getOutputStream() ){
while (true){
//要做的事情,仍然是四个步骤
//1.从控制台读取字符串
System.out.println("->");
String request = scanner.next();
//2.根据读取的字符串,构造请求,把请求发给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
printWriter.flush();//如果不刷新,可能服务器无法及时看到
//3.从服务器读取响应,并解析
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
//4.把结果显示到控制台上
System.out.printf("req: %s, resp: %s\n",request,response);
}
}
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
多线程
为啥UDP版本的程序没用多线程就正常
因为UDP不需要处理连接~UDP只要一个循环,就可以处理所有客户端的请求
但是此处TCP既要处理连接,又要处理一个连接中的若干次请求,就需要两个循环,里层循环就会英雄到外层循环
主线程,循环使用accept,当客户端连接上来的时候,就直接让主线程创建一个新线程,由新线程负责对客户端的若干个请求,提供服务,在新线程里,通过while循环来处理请求,这个时候,多线程是并发执行的关系(宏观上看起来同时执行),就是各自执行各自的,就不会相互干扰
线程池也可以解决
前端后端通过网络来交互的
在这个交互的过程中,就需要约定好,前端发啥呀的数据,后端对应的数据
设计一个应用协议
明确传输的信息
明确数据的组织格式