网络原理初始
网络原理和网络编程[重要]
网络能够跨主机通信!
我们未来工作,很可能是成为后端开发工程师,写服务器,和客户端通信,肯定会涉及到网络.
网络初始
计算机网络的由来
~~ 计算机网络这是计科相关专业最核心的专业课!!!
计算机是咋来的??最初是用来计算弹道导弹的轨迹 => 为了打仗
计算机网络是咋来的?同理,也是打仗打出来的!!!
互联网之前,可以通过有线/无线,发电报,有电话.缺点非常明显,通信链路容易被打击.二战之后 ~~ 美苏争霸,冷战
武器不停升级=>核武器, 杀伤力太大了 ~~ 使用核武器,妥妥的把你通信链路,连根拔起.
美国人开始研究,有没有办法,能够搞出一种通信手段,即使是核武器,也打击不了!!
古巴导弹危机
美国人开始在北约,部署核武器,能直接威胁到苏联本土
古巴离美国本土太近了,苏联人就准备在古巴,部署核武器,直接威胁到美国本土最后苏联放弃了,核战争没打起来.上个世纪,80年代左右,网络还处在萌芽阶段.不过,有些学校/研究所,已经尝试把计算机进行相连,可以进行简单的网络通信了.
到了后来,这个时候,时代背景,冷战(局势是非常紧张的)
这个事情完了之后,大国就开始反思,
在这种核威慑下,如何才能保证足够的威慑力??
A和B相互威慑,意思就是A可以对B进行核打击.但是B在承受打击之后,仍然有能力进行反制(对A进行核打击)如果B的反击能够成立,此时意味着A和B之间谁都不敢轻举妄动.
A打击B要打击哪里?
1.政府核心机构 ~~ 重点目标,必然重点保护,想打掉没那么容易!!!
2.打击B的导弹发射井 ~~ 重点目标,必然重点保护 ~~ 想打掉没那么容易!!!
3.打击网络通信系统 => 假设A把B的网络系统打掉了,此时B想反制,肯定是政府核心机构,发起打击命令,由导弹发射井执行~~此时命令可能就传不过去了!!
因此,研究重点,就是研究出一种通信网络,不怕核打击!!!
在上述背景下,互联网,就应运而生了.
关键问题,万一真的受到核打击了,通信链路仍然正常,因此可以发出指令,进行核反击
后来大家就发现了,互联网这个东西,民用更香!!!就衍生出了很多很多的公司和产品,构建成了咱们今天的丰富的互联网世界.
现在互联网已经渗透到生活中的方方面面了,国内的互联网兴起,得是从2000年左右开始算了.
网络编程
基于操作系统提供的socket api来进行网络数据的发送和接收,Java在JVM中又进行了封装.名字上还是叫做Socket,让内核中的传输层协议和咱们应用程序中应用层的协议进行相互通信.
传输层的主要协议: TCP ,UDP
UDP:无连接,不可靠传输,面向数据报,全双工
TCP:有连接,可靠传输,面向字节流,全双工
代码写的风格差别很大.
DatagramSocket: 代表着 socket 文件(操作系统操作网卡,也不是直接操作,而是把网卡抽象成了特殊的文件,称为 socket 文件).
对 UDP 来说,传输数据的基本单位,DatagramPacket指定一个字节数组,作为持有数据的缓冲区.
一次通信涉及到源IP,源端口,目的IP,目的端口,协议类型
死循环在服务器程序中没什么问题,生活中使用的大部分的程序的服务器都是7 * 24小时的.
当然,有的服务器比较牛,不是 7 * 24小时运行…典型代表: 12306 23:00->6:00进行维护
UDP版本的回显服务器
部分相关代码
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
/**
* Created with IntelliJ IDEA.
* Description:
* User: fly(逐梦者)
* Date: 2023-10-17
* Time: 19:22
*/
// UDP 版本的回显服务器
public class UdpEchoServer {
// 网络编程,本质上是要操作网卡
// 但是网卡不方便直接操作,在操作系统内核中,使用了一种特殊的叫做 "socket" 这样的文件来抽象表示网卡
// 因此进行网络通信,势必需要有一个 socket
private DatagramSocket socket = null;
// 对于服务器来说,创建 socket 对象的同时,要让他绑上一个具体的端口号
// 服务器一定要关联上一个具体的端口
// 服务器是网络传输中,被动的一方,如果是操作系统随机分配的端口,此时客户端就不知道这个端口是什么了,也就无法进行通信了.
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
// 服务器不是只给一个客户端提供服务,需要服务很多客服端
while (true) {
// 只要有客户端过来,就可以提供服务
// 1. 读取客户端发送的请求
// receive方法的参数是一个输出型参数,
// 需要先构造好个空白的 DatagramPacket 对象, 交给 receive 来进行填充.
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);// receive内部会针对参数对象填充数据,填充的数据来自于网卡.
// 此时这个 DatagramPacket 是一个特殊的对象,并不方便直接进行处理,可以把这里包含的数据拿出来,构造成一个字符串
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求计算响应,由于是回显服务器,请求和响应相同.
String response = process(request);
// 3. 把响应数据写回到客户端, send 的参数也是 DatagramPacket,需要把这个 Packet 对象构造好.
// 此处构造的响应对象,不能是用空的字节数组构造了,而是要使用响应数据来构造.
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length
, requestPacket.getSocketAddress());
socket.send(responsePacket);
}
}
// 这个方法就表示"根据请求计算响应"
private String process(String request) {
return request;
}
}
代码的注解
服务器的工作流程.
1.读取请求并解析
2.根据请求计算响应
3.构造响应并写回给客户端
客户端这边的代码
注: 端口号用来标识/区分一个进程.因此在同一个主机上,不允许一个端口同时被多个进程使用.
不过,一个进程可以绑定多个端口,进程只要创建多个 socket 对象,就可以分别关联不同的端口.
总结: socket 和 端口号是一对一的,进程和 socket 是一对多的.
对于服务器,端口必须是确定好的,对于客户端来说,端口可以是系统分配的.
虽然客户端可以做到指定端口号,但是不推荐!!! 因为客户端如果显示指定端口,可能就和客户端电脑上的其他程序的端口冲突了,这一冲突就可能导致程序无法正确通信了(运行就会抛出异常,提示绑定端口失败).
问题来了: 为什么服务器这里指定端口就不怕重复呢?
服务器是我们程序猿自己手里的机器,上面运行啥,都是可控的.我们可以安排哪个程序用哪个端口[可控的]
客户端的机器是在用户手里的,不同用户手里的机器,种类繁多,上面运行着哪些程序,也各有不同[不可控]
总结: 服务器的端口是要固定指定的,目的是为了方便客户端找到服务器程序.
客户端的端口是由系统自动分配的,如果手动指定,可能会和客户端其他程序的端口冲突.
服务器不怕冲突的原因,因为服务器上面的程序可控.客户端是运行在用户电脑上,环境更复杂,更不可控.
我们看到的IP地址: 127.0.0.1 => 32位的整数(给计算机看的).
点分十进制(给人看的),每个部分,范围是 0-255 一个字节.
DatagramSocket这个类的receive能阻塞,是因为操作系统原生提供的APl (recv)就是阻塞的函数.
这里的阻塞不是Java 实现的,而是系统内核里实现的.
系统里对于IO操作本身就有这样阻塞等待的机制.
哪个线程如果进行IO操作,在IO完成之前,就会自动把对应的线程放到阻塞队列中,暂时不参与调度.
对于客户端服务器程序来说一个服务器要给很多客户端提供服务的,我们也就需要构造出多个客户端来进行测试.
由于 IDEA 默认只能启动一个客户端所以需要稍微调整一下,让idea 能启动多个客户端.
修改后的运行结果展示
注: 当前的客户端和服务器程序,都是在博主自己的笔记本上跑的,而实际上,网络存在的意义,是跨主机通信的.
但是如果我把客户端的代码发给别人,别人是连不上我笔记本上的服务器程序的.
但是有解决办法: 让其他人连上“云服务器”这样的特殊电脑,“云服务器”有外网 IP,任何一个连上网络的设备都能访问(博主的笔记本电脑是没有外网IP,只能在局域网内部访问,所以别人是连不上我笔记本上的服务器程序的)
把程序部署到云服务器(了解即可)
1.把java程序打包.
上述操作的结果
2.对云服务器(Linux系统)相关操作
将IP地址更改为云服务器的IP
博主的笔记本电脑和云服务器(另一个主机)跨越网络进行通讯了.
注: 可以把代码发给其他人,这样你们观看云服务器上的你们发送的信息进行交流了.
相关的完整代码
UdpEchoClient.java
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: fly(逐梦者)
* Date: 2023-10-17
* Time: 19:23
*/
// UDP 版本的 回显客户端
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp = null;
private int serverPort = 0;
// 一次通信,需要有两个ip,两个端口
// 此时客户端的 ip 是 127.0.0.1 已知的
// 客户端的 port 是系统自动分配的
// 服务器 ip 和 端口 也需要告诉客户端,才能顺利把消息发到服务器
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true) {
// 1.从控制台读取数据
System.out.print("> ");
String request = scanner.next();
if (request.equals("exit")) {
System.out.println("goodbye");
break;
}
// 2. 构造成 UDP 请求
// 构造这个 Packet 的时候,需要把 serverIP 和 port 都传入过来,但是此处 IP 地址需要填写的是一个 32 位的整数形式.
// 上述的 IP 地址是一个字符串,需要使用 InetAddress.getByName 来进行一个转换.
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
// 3.读取服务器的 UDP 响应, 并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength(),"utf8");
// 4. 把解析好的结果显示出来
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client=new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
UdpEchoServer.java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
/**
* Created with IntelliJ IDEA.
* Description:
* User: fly(逐梦者)
* Date: 2023-10-17
* Time: 19:22
*/
// UDP 版本的回显服务器
public class UdpEchoServer {
// 网络编程,本质上是要操作网卡
// 但是网卡不方便直接操作,在操作系统内核中,使用了一种特殊的叫做 "socket" 这样的文件来抽象表示网卡
// 因此进行网络通信,势必需要有一个 socket
private DatagramSocket socket = null;
// 对于服务器来说,创建 socket 对象的同时,要让他绑上一个具体的端口号
// 服务器一定要关联上一个具体的端口
// 服务器是网络传输中,被动的一方,如果是操作系统随机分配的端口,此时客户端就不知道这个端口是什么了,也就无法进行通信了.
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
// 服务器不是只给一个客户端提供服务,需要服务很多客服端
while (true) {
// 只要有客户端过来,就可以提供服务
// 1. 读取客户端发送的请求
// receive方法的参数是一个输出型参数,
// 需要先构造好个空白的 DatagramPacket 对象, 交给 receive 来进行填充.
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);// receive内部会针对参数对象填充数据,填充的数据来自于网卡.
// 此时这个 DatagramPacket 是一个特殊的对象,并不方便直接进行处理,可以把这里包含的数据拿出来,构造成一个字符串
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2. 根据请求计算响应,由于是回显服务器,请求和响应相同.
String response = process(request);
// 3. 把响应数据写回到客户端, send 的参数也是 DatagramPacket,需要把这个 Packet 对象构造好.
// 此处构造的响应对象,不能是用空的字节数组构造了,而是要使用响应数据来构造.
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length
, requestPacket.getSocketAddress());
socket.send(responsePacket);
// 4. 打印一下, 当前这次请求响应的处理中间结果
System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString()/*获取到packet里面的ip*/,
requestPacket.getPort()/*获取到里面的端口*/,request,response);
}
}
// 这个方法就表示"根据请求计算响应"
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
// 端口号的指定,可以随便指定
// 1024 -> 65535 这个范围里随便挑个数字就行了
UdpEchoServer server=new UdpEchoServer(9090);
server.start();
}
}
实现一个简单的翻译程序
回显服务器,缺少业务逻辑
就在上述代码的基础上稍作调整,就可以实现一个“查词典”的服务器(英文单词,翻译成中文解释).
注: 代码主要是复用了之前 EchoServer 的代码,然后重写 process 就行了.
不同的服务器,对应的服务器是不同的,因此,处理的流程就是不同的,对于一些复杂的服务器, process 里面可能要运行几万行,几十万行……
UdpDictServer.java
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
/**
* Created with IntelliJ IDEA.
* Description:
* User: fly(逐梦者)
* Date: 2023-10-21
* Time: 0:01
*/
// 对于 DictServer 来说, 和 EchoServer 相比,大部分的东西都是一样的.
// 主要是 "根据请求计算响应" 这个步骤不太一样.
public class UdpDictServer extends UdpEchoServer {
private Map<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
// 给这个 dict 设置内容
dict.put("cat", "小猫");
dict.put("dog", "小狗");
dict.put("fly", "廖飞洋");
// 当然,这里可以无限多的设置键值对......
}
@Override
public String process(String request) {
// 查词典的过程
return dict.getOrDefault(request, "当前单词没有查到结果!");
}
}
运行结果
端口冲突
一个端口只能被一个进程使用,如果有多个使用,就不被允许.
Address already in use: Cannot bind
这里的 bind 是操作系统原生的API,这个API本身就是起到的“绑定IP+端口”,Address 表示的含义就相当于“IP+端口”.
TCP的相关 API 的使用
TCP提供的API主要是两个类:
ServerSocket专门给服务器使用的Socket对象
Socket是既会给客户端使用,也会给服务器使用
TCP不需要一个类来表示“TCP数据报”,因为TCP不是以数据报为单位进行传输的,是以字节的方式,进行流式传输.
ServerSocket API
~~ ServerSocket
是创建TCP服务端Socket的API
ServerSocket
构造方法
方法签名 | 方法说明 |
---|---|
ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket
方法
方法签名 | 方法说明 | |
---|---|---|
Socket accept() | 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket 对象,并基于该Socket建立与客户端的连接,否则阻塞等待 | 相当于“接电话”,接了电话,会返回一个Socket对象,通过这个Socket对象和客户端进行沟通. |
void close() | 关闭此套接字 |
Socket API
在服务器这边,是由accept返回的,在客户端这边,是由我们代码里构造的,构造的时候指定一个IP和端口号(此处的IP和端口号是服务器的IP和端口),有了这个信息,就能够和服务器建立连接了.
Socket
构造方法
方法签名 | 方法说明 |
---|---|
Socket(String host, int port) | 创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket
方法
方法签名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
InputStream getInputStream() | 返回此套接字的输入 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
InputStream getInputStream()
,OutputStream getOutputStream()
:进一步通过Socket对象,获取到内部的流对象,借助流对象进行发送/接受.
TCP版本的回显服务器
相关代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: fly(逐梦者)
* Date: 2023-10-21
* Time: 15:26
*/
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("启动服务器");
while (true) {
/* accept效果是"接收连接",前提是得有客户端来建立连接!
客户端在构造Socket对象的时候,就会指定服务器的IP和端口.
如果没有客户端来连接,此时accept就会阻塞.
*/
Socket clientSocket = serverSocket.accept();
// 使用这个 clientSocket和具体的客户端进行交流.
processConnection(clientSocket);
}
}
// 使用这个方法来处理一个连接
// 这一个连接对应到一个客户端,但是这里可能会涉及到多次交互
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
// 基于上述 socket 对象和客户端进行通信
try(InputStream inputStream=clientSocket.getInputStream();
OutputStream outputStream=clientSocket.getOutputStream()) {
// 由于要处理多个请求和响应,也是使用循环来进行的
while (true){
// 1.读取请求
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()){
// 没有下一个数据,说明读完了(客户端关闭了连接)
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
// 注: 此处使用 next 是一直读取到换行符/空格/其他空白符结束,但是最终返回结果里不包含上述空白符.
String request=scanner.next();
// 2.根据请求构造响应
String response=process(request);
// 3.返回响应结果
// outputStream 没有 write String 这样的功能,可以把 String 里的字节数组拿出来,进行写入
PrintWriter printWriter=new PrintWriter(outputStream);
// 此处使用 println 来写入,让结果中带有一个 \n,方便接受端来接受解析
printWriter.println(response);
// flush 用来刷新缓冲区,保证当前写入的数据,确实是发送出去了.
printWriter.flush();
System.out.printf("[%s %d] req: %s; resp: %s \n",
clientSocket.getInetAddress().toString(),
clientSocket.getPort(),
request,response);
}
}catch (IOException e){
e.printStackTrace();
}finally {
// 把 close 放到 finally 里面的,保证一定你能执行到!
clientSocket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server=new TcpEchoServer(9090);
server.start();
}
}
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
这个代码中,用到一个clientSocket.此时任意一个客户端连上来,都会返回/创建一个Socket对象.(Socket就是文件)每次创建一个clientSocket对象,就要占用一个文件描述符表的位置.再加上因为每个客户端都有一个,此处的clientSocket的数量就会非常多,因此在使用完毕之后,就需要进行"释放".
代码缺陷: 当前的这个TCP server 有一个致命缺陷,一次只能处理一个客户端.
主要是主动发起请求的一方,就是服务器.只要是被动的一方,就是服务器.所以一个服务器即可是服务器,也可以是客户端.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* Created with IntelliJ IDEA.
* Description:
* User: fly(逐梦者)
* Date: 2023-10-21
* Time: 18:48
*/
public class TcpEchoClient {
private Socket socket=null;
public TcpEchoClient(String serverIp,int serverPort) throws IOException {
// Socket 构造方法,能够识别 点分十进制格式的IP地址,比DatagramPacket更方便.
// new 这个对象的同时,就会进行 TCP 的连接操作.
socket=new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("客户端启动!");
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream()) {
while (true){
// 1.先从键盘读取用户输入的内容
System.out.println(">");
String request=scanner.next();
if (request.equals("exit")){
System.out.println("goodbye");
break;
}
// 2.把读到的内容构造成请求,发送到服务器
PrintWriter printWriter=new PrintWriter(outputStream);
printWriter.println(request);
// 此处加个 flush,保证数据确实发送
printWriter.flush();
// 3.读取服务器的响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4.把响应内容显示到界面上
System.out.println(response);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}