认识网络(一)

一、网络初识

1.网络协议

形如上面图示中的一样,网络通信指的是两个主机之间,进行数据传输的过程,但是,实际生活中咱们的网络通信是这样的吗?

其实并不是,主机A和主机B之间是有着很复杂的一段过程的,主机和主机之间会经历类似如下的一个过程:

数据为什么能准确的从主机A送达到主机B呢?

这里我们主要依靠的就是“网络协议”,通过一些既定的规则去编排数据,再用相同的规则去解析数据,那么最终数据就能够完整地被传输。

2.协议分层

为什么要引入协议分层?

比如说,一个公司是存在着上下级关系的,普通员工是不能够直接去跨级汇报事情给公司董事长的,必须一级一级的汇报,否则董事长就忙不过来了,那么网络协议也是类似,为了避免跨层级调用引起的混乱,人为的将协议进行分层,上层协议调用下层协议,下层协议服务上层协议,降低耦合,提高效率。

协议分层分为两种:

1.OSI七层网络协议模型(只出现在教科书中,了解即可)

2.TCP/IP五层网络模型(重点)

物理层:描述的网络通信中的一些基础设施需要遵守的规范(如网线、网口等的样子)

数据链路层:描述相邻节点之间,数据如何传输

网络层:路径规划(如上海->西安,有很多不同的路线走法,网络层这里就需要规划该走哪条路线)

传输层:描述数据从哪里来,到哪里去

应用层:描述数据用来干什么用

3.数据传输的基本流程

数据传输的核心步骤有两个:1.封装 2.分用

以QQ发信息为例:

用户A给用户B发信息


发送方的情况:

1.应用层:

QQ应用程序,从输入框获取到你所输入的信息,构造成应用层数据报(根据应用层协议),在这之后,引用程序就会调用传输层提供的接口,把数据报交给传输层处理。

2.传输层:

传输层的协议有很多,最主要的是TCP和UDP协议,这里以UDP为例

应用层传到传输层的数据,通过UDP协议,生成一个UDP数据报

UDP并不关心应用层发来的数据里面有什么,都是些什么内容,只是把应用层的数据当作一个字符串,构造出一个UDP数据报,这个数据报包含两个部件,分别是UDP报头和UDP载荷。

UDP报头包含两个主要信息:1.源端口 2.目的端口

传输层再进一步将UDP数据报交给网络层

3.网络层

网络层最主要的协议是IP协议

IP协议根据自己的格式,构造出一个IP数据报

IP协议同样也不关心传输层发来的是什么数据,只是在数据的前面拼接上一个IP报头

IP报头包含两个最主要信息:1.源IP 2.目的IP

此时我们可以引出网络通信的“五元组”:1.源IP 2.源端口 3.目的IP 4.目的端口 5.协议类型

接下来,我们的网络层继续将数据上传给数据链路层

4.数据链路层

最重要的协议:以太网(平时上网需要插的一个网线)

以太网又会针对IP数据报进一步的进行封装,再添加上帧头和帧尾

最后继续将IP数据报传给物理层

5.物理层

以太网的数据报本质上都是二进制的数据,通过硬件设备,如网卡等,将这些二进制数据转换成光信号/电信号/电磁波等。

到这里,主机A就完成了发送过程


接受方的情况(先不考虑中间过程)

1.物理层

一些硬件设备,如网卡等接收到电信号/光信号/电磁波等,再把这些信号进行解调,得到了一串二进制数据序列,也就是以太网数据报,这个数据被递交给上一层,数据链路层。

2.数据链路层

数据链路层的以太网协议就开始对数据进行解析,将帧头和帧尾解析,再将载荷交给上层,即网络层。

3.网络层

IP协议针对载荷进行解析,去掉IP报头,取出载荷,进一步交给传输层。

4.传输层

根据IP报头中的字段,就知道当前是一个UDP数据报,交给UDP处理,UDP针对数据报进行解析,去掉报头,取出载荷,再进一步交给应用程序。

5.应用层

UDP报头中,有一个字段是目的端口,目的端口找到关联的应用程序QQ,将数据交给它,QQ根据它的应用层协议,进行解析,再把这里的数据显示到界面上。

这样完整的十个过程就王成了QQ通信。

主机A从上到下添加报头的过程就叫做封装

主机B从上到下解析报头的过程就叫做分用

二、网络编程

网络传输层协议主要有两个:

1.UDP:不可靠传输,无连接,面向字符流,全双工

2.TCP:可靠传输,有连接,面向字节流,全双工

1.UDP

UDP主要有两个核心api:

1)DatagramSocket

在操作系统中,使用文件的概念去管理一些软硬件资源,如网卡等,表示网卡的这类文件,称为socket文件,要进行网络通信就必须有socket对象。

了解一个类,我们先了解下它的构造方法,DatagramSocket提供了两个构造方法,一个指定了port端口号,一个没有指定,我们之前了解知道,网络通信五元组分别是:源端口、源IP、目的端口、目的IP、协议类型,客户端和服务器在进行发送信息的时候,就需要一个端口来进行对端传输。

这里需要注意的是,客户端端口号系统会自动分配,服务器端口号需要自行指定。

为什么要这样安排呢?

一个客户端主机上,有很多的程序并发运行,我们并不能很明确知道某个端口是否是空闲的,而让系统自行去分配,这样更为的明智,而服务器是我们能够控制的,我们将服务器的程序安排好,就能很快找到空闲的端口了。举个例子:我在马路边开了个饭店,这个饭店的地址就必须是固定的且明确的,方便别人找到,客人来我这吃饭,它们坐的位置不是固定的,哪里空就坐哪里,饭店就相当于服务器,客人的位置就相当于客户端。

2)DatagramPacket

DatagramPcket表示了一个数据报,代表了系统中设定的UDP数据报的二进制结构

构造方法:

3)实例

到目前为止,我们了解了UDP的两个核心api,那我们就要开始进行实例训练了,我们这里做一个简单的客户端-服务器,叫做“回显服务器”,客户端发送什么,服务器就返回什么,但是实际开发中,服务器返回值都是比较复杂的,这里只是简单的一个应用。

服务端代码:

//回显服务器 //客户端发的请求是什么,服务器返回的响应就是什么 public class UdpEchoServer { public DatagramSocket socket = null; public UdpEchoServer(int port) throws SocketException { socket = new DatagramSocket(port);//对象产生失败的原因就是端口号被占用 } //使用这个方法启动服务器 public void start() throws IOException { System.out.println("服务器启动!"); while(true){ //反复针对客户端的请求进行处理 //一个服务器,运行过程中,主要是三个核心环节 //1.读取请求,并解析 DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096); socket.receive(requestPacket); String request = new String(requestPacket.getData(),0, requestPacket.getLength()); //2.根据请求,计算出响应(由于这里是回显服务器,就不关心这个流程,请求是啥,返回的响应就是啥,但是针对商业级服务器,代码主要完成的是这个步骤) String response = process(request); //3.把响应写回给客户端 DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress()); socket.send(responsePacket); //设置日志,记录服务端数据变化 System.out.printf("[%s:%d] req:%s,resp:%s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response); } } public String process(String response){ return response; } public static void main(String[] args) throws IOException { UdpEchoServer udpEchoServer = new UdpEchoServer(9090); udpEchoServer.start(); } }

客户端代码:

public class UdpEchoClient { public DatagramSocket socket = null; public String serverIp; public int port; //服务器的ip和端口传入 public UdpEchoClient(String serverIp,int port) throws SocketException { this.serverIp = serverIp; this.port = port; //这个new操作,不再指定端口,由系统自动分配 socket = new DatagramSocket(); } public void start() throws IOException { System.out.println("客户端启动!"); Scanner scanner = new Scanner(System.in); while(true){ //1.从客户端控制台输入内容 System.out.print("->:"); String request = scanner.next(); //2.构造请求对象,传入服务器 DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),port); socket.send(requestPacket); //3.读取服务器响应,并解析出响应内容 DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096); socket.receive(responsePacket); String response = new String(responsePacket.getData(),0,responsePacket.getLength()); //4.显示到屏幕上 System.out.println(response); } } public static void main(String[] args) throws IOException { UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090); udpEchoClient.start(); } }

2.TCP

TCP也同样提供了两个核心api:

1)ServerSocket

这个主要是给服务器使用。

2)Socket

这个不仅服务器可以使用,客户端也会用。

这里两个api的构造方法可以在java官方文档中查看详情,这里主要不是为了讲解这些构造方法的,所以先跳过,主要需要知道的就是前两个构造方法。

3)实例

TCP这里我们同样实现一个回显服务器,但是这里我们需要详解一些代码。

服务端:

public class TcpEchoServer { public ServerSocket serverSocket = null; //这个操作用来绑定端口号 public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } //启动服务器 public void start() throws IOException { System.out.println("服务器启动!"); while(true){ Socket clientSocket = serverSocket.accept(); //需要使用多线程,否则会使一个服务器只能服务一个客户端的情况 Thread t = new Thread(() -> { try { processConnection(clientSocket); } catch (IOException e) { throw new RuntimeException(e); } }); t.start(); } } //通过这个方法来处理一个连接的逻辑 public 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 scanner = new Scanner(inputStream); if(!scanner.hasNext()){ //读取完毕,客户端下线 System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort()); break; } //这个代码暗含一个约定,客户端发过来的请求,得是文本数据,同时,还得带有空白符(包括但不限于,换行,回车,空格等)做为分隔 String request = scanner.next(); //2.根据请求,计算响应 String response = process(request); //3.把响应写回客户端,把OutputStream使用PrintWriter包裹一下,方便进行发数据 PrintWriter writer = new PrintWriter(outputStream); //使用PrintWriter的println方法,把响应返回给客户端 //此处使用println,而不是print,是为了给结尾加上\n,方便客户端读取响应,使用scanner.next读取 writer.println(response); //PrintWriter内置了缓冲区,通过手动刷新缓冲区,确保数据真的通过网卡发出去了,而不是残留在内存缓冲区中 writer.flush(); //日志,打印当前的请求详情 System.out.printf("[%s:%d] req:%s, resq:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response); } }finally { //clientSocket被反复创建,所以需要关闭 clientSocket.close(); } } public String process(String request){ return request; } public static void main(String[] args) throws IOException { TcpEchoServer tcpEchoServer = new TcpEchoServer(9090); tcpEchoServer.start(); } }
a)针对服务端代码解析

我们映入眼帘的第一个与UDP回显服务器不同的是这里,这是什么意思?accept了一个什么东西?

我们之前就知道TCP是有连接传输,客户端和服务器建立好连接以后就会存入一个队列,而这个accept就是将存放在内核中的连接拿出来进行使用。

第二个与UDP回显服务器不同的是这里,为什么使用多线程

因为TCP是有连接的,如果单线程状态下,连续的两个客户端同时发送请求,就会有一个进入堵塞,但我们使用多线程的话,这样就一个服务器能同时处理多个客户端发送过来的请求。这里我们还可以进行优化,多线程中了解了线程池的概念,所以我们这里可以使用线程池来写,代码如下:

//创建一个不固定数量的线程池 public ExecutorService service = Executors.newCachedThreadPool(); //启动服务器 public void start() throws IOException { System.out.println("服务器启动!"); while(true){ Socket clientSocket = serverSocket.accept(); //需要使用多线程,否则会使一个服务器只能服务一个客户端的情况 // Thread t = new Thread(() -> { // try { // processConnection(clientSocket); // } catch (IOException e) { // throw new RuntimeException(e); // } // }); // t.start(); service.submit(new Runnable() { @Override public void run() { try { processConnection(clientSocket); } catch (IOException e) { throw new RuntimeException(e); } } }); } }

客户端:

public class TcpEchoClient { public Socket socket = null; // 要和服务器通信,就需要先知道,服务器所在的位置 public TcpEchoClient(String serverIp,int serverPort) throws IOException { //这个new操作完成之后,就完成了tcp连接的建立 socket = new Socket(serverIp,serverPort); } public void start() throws IOException { System.out.println("客户端启动了!"); Scanner scannerConsole = new Scanner(System.in); try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { while (true) { //1.从控制台输入字符串 System.out.print("->:"); String request = scannerConsole.next(); //2.把请求发送给服务器 PrintWriter printWriter = new PrintWriter(outputStream); //使用println带上换行,后续服务器读取请求,就可以使用scanner.next()来获取了 printWriter.println(request); printWriter.flush(); //3.从服务器读取响应 Scanner scannerNetwork = new Scanner(inputStream); String response = scannerNetwork.next(); //4.把响应打印出来 System.out.println(response); } } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090); client.start(); } }

原创文章,作者:优速盾-小U,如若转载,请注明出处:https://www.cdnb.net/bbs/archives/30771

(0)
优速盾-小U的头像优速盾-小U
上一篇 2025年6月18日 18:42
下一篇 2025年6月18日 23:04

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

优速盾注册领取大礼包www.cdnb.net
/sitemap.xml