
网络互连将多台计算机连接在一起完成数据共享。根据网络互连的规模分为局域网和广域网局域网覆盖范围小例如家里同用一个wifi1.网线直接连接2.基于集线器组建3.基于交换机组建4.基于交换机和路由器组建广域网覆盖范围广例如分公司以专线方式连接起来即称广域网。将多个局域网连接起来就形成广域网其实就是互联网网络通信基础我们进行网络互连的目的就是为了进行网络通信即数据传输1.IP地址简单来说就是用于定位主机的网络地址标识一台设备所在位置ip地址通常是一个32位 的二进制数如01100100.00000100.00000101.000001102.端口号端口号用于定位主机的进程一台主机有多程序使用网络通过端口号区分是哪个程序。类似于发送快递收货地址IP地址收件人端口号3.认识协议程序收到数据之后如何理解数据的含义呢是由网络协议针对双方数据发送和接受进行数据格式约定。在TCP,IP协议使用五元组标识网络通信源IP源端口号目的IP目的端口号协议号标识发送接受进程约定的数据格式协议分层将一个大的协议拆分成小的协议只有相邻两层协议可以交互下层协议给上层协议提供服务上层协议可以调用下层协议。作用1.封装上层协议调用下层协议不需要了解下层协议的细节分用数据接收时候从物理层开始每层剥用自己的头部信息最后把原始数据给应用层。2.解耦可以直接替换某层协议对整体工作过程影响小真实互联网如何分层OSI七层网络模型7.应用层6.表示层5.会话层4.传输层3.网络层2.数据链路层1.物理层但OSI七层网络模型复杂不实用实际使用的是其中的5层模型TCP/IP五层模型,四层模型把数据链路层和物理层看做一层5.应用层拿到数据如何使用4.传输层考虑两个设备进行通信的起点和终点关心的是进程与进程如何通信例如设备A浏览器进程送到设备BWeb服务进程3.网络层两个任意设备如何进行通信这中间可能隔着许多交换机、路由器2.数据链路层完成两个相邻设备如何通信例如把电脑连接到交换机/路由器上1.物理层规定网络通信一些硬件设备符合的要求网络设备所在分层主机工作层级实现了物理层到应用层作用通过应用程序满足具体业务需求全部流程都要参与路由器工作层级实现了物理层到网络层只关心哪个设备到哪个设备不关心是给微信还是浏览器作用是组建局域网进行网络数据包转发交换机工作层级实现物理层到数据链路层核心作用是对路由器接口进行扩展在同一个局域网内设备的数据转发不需要考虑跨网问题4.网络数据通信基本流程(例如发一段信息发送过程1.应用程序获取用户输入构造应用层的数据包其遵守应用层协议通常是程序员定义2.应用层调用传输层提供的接口api)-socket api把数据交给传输层。传输层协议主要是TCP和UDP传输层拿到数据包构造出传输层数据包即在之前数据包的基础上加上一个TCP报头。3.传输层拿到数据后调用网络层api把传输层数据包交给网络层网络层继续处理。网络层主要协议IP协议网络层拿到数据包构造出网络层数据包,即在之前数据包的基础上加上一个IP报头4.IP协议继续调用数据链路层api把IP数据包交给数据链路层数据链路层主要协议以太网协议数据链路层拿到数据包构造出数据链路层数据包,即在之前数据包的基础上加上一个以太网报头和尾巴5.以太网把数据又给物理层硬件设备网卡网卡最终把上述二进制数据以光信号/电信号/电磁波形式发送出去。第二阶段数据包在网络传输【这一步就是到达对方电脑的过程】数据包带着目标 IP经过多个路由器转发路由器收到以太网帧 → 拆掉二层以太网头看里面 IP 包的目标 IP查询路由表决定下一跳该发给哪个设备重新封装新的二层以太网头部转发给下一个路由器一跳一跳不断转发直到这个数据包抵达接收方服务器 / 电脑的网卡✅ 到这里数据包成功到达对方电脑了第三阶段接收方拆包就是你写的 5 步逐层向上解封装数据到达接收方网卡光 / 电信号还原二进制交给数据链路层数据链路层剥掉以太网报头、尾部校验载荷交给网络层网络层剥掉 IP 头部载荷交给传输层传输层剥掉 TCP 头部载荷交给应用层应用程序解析应用层数据拿到原始业务数据网络编程套接字网络编程网络的主机通过不同的进程以编程方式实现网络通信网络数据传输发送端数据的发送进程接收端数据的接收进程二者并称收发端服务端在常见的网络数据传输下提供服务一方进程为服务端可以对外服务。客户端获取服务一方进程为客户端。Socket套接字系统提供的网络通信技术是基于TCP/IP协议的网络通信基本操作单位。基于Socket套接字的网络程序开发就是网络编程。Socket套接字针对传输层协议主要分为3类1.流套接字使用传输层TCP协议TCP有连接可靠传输面向字节流全双工2.数据报套接字使用UDP协议UDP无连接不可靠传输面向数据报全双工3.原始套接字UDP数据报套接字编程DatagramSocketDatagramSocket是java针对UDP网络通信的封装它把复杂的抽象的网卡操作抽象成文件操作让开发者可以像读写文件一样收发网络数据同时通过端口号区分不同的应用程序。DatagramSocket的方法方法签名方法说明DatagramSocket()创建一个 UDP 数据报套接字的 Socket绑定到本机任意一个随机端口一般用于客户端DatagramSocket(int port)创建一个 UDP 数据报套接字的 Socket绑定到本机指定的端口一般用于服务端方法签名方法说明void receive(DatagramPacket p)从此套接字接收数据报如果没有接收到数据报该方法会阻塞等待void send(DatagramPacket p)从此套接字发送数据报包不会阻塞等待直接发送void close()关闭此数据报套接字DatagramPacket表示一个完整的UDP数据报方法签名方法说明DatagramPacket(byte[] buf, int length)构造一个用于接收数据报的DatagramPacket接收的数据保存在字节数组buf中接收长度为lengthDatagramPacket(byte[] buf, int offset, int length, SocketAddress address)构造一个用于发送数据报的DatagramPacket发送的数据为字节数组buf中从offset开始、长度为length的部分address指定目标主机的 IP 和端口号客户端主动发起请求的一端服务器主动接收请求并且提供服务的一方请求客户端向服务器发送的需求响应服务器根据请求返回的结果UDP Echo Serverimport java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; public class UdpEchoServer { private DatagramSocket socketnull; UdpEchoServer(int port) throws SocketException { socket new DatagramSocket(port); } public void start() throws IOException { //启动服务器 System.out.println(服务器启动); while (true){ DatagramPacket requestPacketnew DatagramPacket(new byte[4096],4096); socket.receive(requestPacket); String requestnew String(requestPacket.getData(),0,requestPacket.getLength()); String responseprocess(request); DatagramPacket responsepacketnew DatagramPacket(request.getBytes(), 0,request.getBytes().length,requestPacket.getSocketAddress()); socket.send(responsepacket); } } private String process(String request) { return request; } public static void main(String[] args) throws IOException { UdpEchoServer servernew UdpEchoServer(9090); server.start(); } }1.初始化服务器private DatagramSocket socketnull;socket对象代表网卡文件读这个文件等于从网卡收数据写这个文件代表从网卡发数据UdpEchoServer(int port) throws SocketException {socket new DatagramSocket(port);}socket对象实例化并且指定一个端口给服务器使用2.主循环分为三步循环一次就相当于处理一次请求处理请求过程典型服务器分为三步1.读取请求并且解析//DatagramPacket表示一个UDP数据报此处传入字节数组就是保存UDP的载荷部分DatagramPacket requestPacketnew DatagramPacket(new byte[4096],4096);构造一个用于接收数据报的DatagramPacket接收的数据保存在字节数组buf中接收长度为length//接收客户端的数据报socket.receive(requestPacket);receive会发生阻塞客户端请求发来receive才会返回//读到的二进制数据转成字符串只构造有效部分String requestnew String(requestPacket.getData(),0,requestPacket.getLength());new String(字节数组, 起始偏移量, 数据长度)requestPacket.getData()拿到requestPacket的字节数组 requestPacket.getLength()有效数据的长度2.根据请求计算响应此处是回显服务器这个环节相当于省略)String responseprocess(request);private String process(String request) { return request; }3.把响应返回给客户端//根据response构造DatagramPacke数据报t发送给客户端DatagramPacket responsepacketnew DatagramPacket(request.getBytes(), 0,request.getBytes().length,requestPacket.getSocketAddress());构造一个用于发送数据报的DatagramPacket发送的数据为字节数组buf中从offset开始、长度为length的部分address指定目标主机的 IP 和端口号//response.getBytes.length拿到字节数组的长度而不是使用字符串的长度//requestPacket.getSocketAddress()从收到的数据报中获取客户端的地址和端口使得服务器能够准确将响应发回发送请求的客户端// 发送数据报包socket.send(responsepacket);4.打印日志System.out.printf([%s:%d] req:%s,resp:%s\n,requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);DatagramPacket的三个方法1getAddress只得到IP2getPort只得到端口3getSocketAddress同时拿到IP和端口IP和端口通过一个InetAddress对象表示UDP Echo Clientimport java.io.IOException; import java.net.*; import java.util.Scanner; public class UdpEchoClient { String serverIP; int serverPort; DatagramSocket socketnull; public UdpEchoClient(String serverIP,int serverPort) throws SocketException, UnknownHostException { this.serverIPserverIP;//目的IP this.serverPortserverPort;//目的接口 socketnew DatagramSocket();//源Ip客户端所在主机的ip源端口是随便搞一个端口操作系统分配剩余端口 } public void start() throws IOException { Scanner scannernew Scanner(System.in); System.out.println(客户端启动); while (true){ //1.从用户台读取输入的内容 System.out.println(-);//命令提示符提示用户要输入字符 String requestscanner.next(); //2.构造对象并且发送给服务器 //把请求发送给服务器需要构造DatagramPacket对象构造过程不光构造载荷还要设置服务器的IP和端口号 DatagramPacket requestPacketnew DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIP),serverPort); socket.send(requestPacket); //3.读取服务器响应并且解析响应内容 DatagramPacket responsePacketnew DatagramPacket(new byte[4096],4096); socket.receive(responsePacket); String reponsenew String(responsePacket.getData(),0,responsePacket.getLength()); System.out.println(reponse); } } public static void main(String[] args) throws IOException { UdpEchoClient udpEchoClientnew UdpEchoClient(127.0.0.1,9090); udpEchoClient.start(); } }127.0.0.1是特殊的IP表示当前这个主机客户端的目的端口一定与服务器的端口相同TCP流套接字ServerSocket专门给服务器使用作用绑定端口等待客户端发起连接返回一个 Socket 对象用于通信。构造方法绑定指定端口开启监听 ServerSocket serverSocket new ServerSocket(8888);accept()→最关键Socket socket serverSocket.accept();阻塞方法一直等待直到有客户端连接才返回返回值与客户端通信的Socket 对象Socket客户端 服务器都可以使用作用建立 TCP 连接通过输入流、输出流实现数据收发。构造方法客户端用java运行// 连接指定IP端口的服务端 Socket socket new Socket(127.0.0.1, 8888);getInputStream()→接收数据java运行InputStream in socket.getInputStream();获取输入流读取客户端 / 服务端发送的数据getOutputStream()→发送数据java运行OutputStream out socket.getOutputStream();获取输出流向客户端 / 服务端发送数据close()java运行socket.close();关闭连接同时自动关闭输入 / 输出流TCP Echo Serverimport java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; public class TCPEchoServer { ServerSocket serverSocketnull; public TCPEchoServer(int port) throws IOException { serverSocketnew ServerSocket(port); } //启动服务器 public void start() throws IOException { System.out.println(服务器启动); while (true){ Socket clientsocketserverSocket.accept(); processConnection(clientsocket); } } //通过此方法处理一个连接的逻辑 private void processConnection(Socket clientsocket) throws IOException { System.out.printf([%s:%d]客户端上线!\n,clientsocket.getInetAddress(),clientsocket.getPort()); //接下来就可以读取请求根据请求计算响应返回响应三步走socket对象内部包含了两个字节流对象可以把这两个字节流对象获取到完成后续的读 try(InputStream inputStreamclientsocket.getInputStream(); OutputStream outputStreamclientsocket.getOutputStream()){ //一次连接可能多次请求 while(true){ //1.读取请求并且解析为了读取方便直接用Scanner Scanner scannernew Scanner(inputStream); if(!scanner.hasNext()){ //读取完毕客户端下线 System.out.printf([%s:%d]客户端下线!\n,clientsocket.getInetAddress(),clientsocket.getPort()); break; } //代码暗含一个约定客户端发过来的请求得是文本数据 String questscanner.next(); //2.根据请求计算响应 String responseprocess(quest); //3.把响应写回客户端 PrintWriter writernew PrintWriter(outputStream); writer.println(response); writer.flush();//刷新缓冲区 //打印日志 System.out.printf([%s:%d]req:%s,resp:%s!\n,clientsocket.getInetAddress(),clientsocket.getPort(),quest,response); } } catch (IOException e) { throw new RuntimeException(e); }finally { //最后加上close,确保当前socket被及时关闭 clientsocket.close(); } } private String process(String quest) { return quest; } public static void main(String[] args) throws IOException { TCPEchoServer tcpEchoServernew TCPEchoServer(9090); tcpEchoServer.start(); } }注意1.clientsocket.close();最后要关闭底层的TCP连接释放操作系统分配的文件描述符表端口等资源避免资源泄露。而UDP没有连接不存在断开连接的操作所以不需要维护连接状态和释放连接资源只需要在最后关闭DatagramSocket释放本地端接口。2. writer.flush();刷新缓冲区强制将缓冲区的数据立即发出因为每次调用write不会立即发送到网络/文件只有缓冲区满了或者流被关闭才会把数据刷出去。TCP Echo Clientimport java.io.*; import java.net.Socket; import java.util.Scanner; public class TCPEchoClient { Socket socketnull; public TCPEchoClient(String serverIP,int serverPort) throws IOException { socketnew Socket(serverIP,serverPort); } public void start(){ System.out.println(客户端启动); Scanner scannerConsolenew Scanner(System.in); try(InputStream inputStreamsocket.getInputStream(); OutputStream outputStreamsocket.getOutputStream()){ while (true){ //1.从控制台输入字符串 System.out.println(-); String questscannerConsole.next(); //2.把请求发给服务器 PrintWriter writernew PrintWriter(outputStream); writer.println(quest); writer.flush(); //3.从服务器读取响应 Scanner scannerNetworknew Scanner(inputStream); String responsescannerNetwork.next(); //4.打印响应 System.out.println(response); } } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws IOException { TCPEchoClient tcpEchoClientnew TCPEchoClient(127.0.0.1,9090); tcpEchoClient.start(); } }一个线程无法响应多个客户端要给每个客户端分配一个线程public void start() throws IOException { System.out.println(服务器启动); ExecutorService executorService Executors.newCachedThreadPool(); while (true){ Socket clientsocketsocket.accept(); Thread threadnew Thread(()-{ try { processconnection(clientsocket); } catch (IOException e) { throw new RuntimeException(e); } }); thread.start(); }为了避免频繁创建销毁线程引入线程池public void start() throws IOException { System.out.println(服务器启动); ExecutorService executorService Executors.newCachedThreadPool(); while (true) { Socket clientsocket socket.accept(); executorService.submit(new Runnable() { Override public void run() { try { processconnection(clientsocket); } catch (IOException e) { throw new RuntimeException(e); } } } ); } }IO复用如果客户端进一步增加此时多线程/线程池就会产生大量线程操作系统内置了IO复用一个线程可以同时负责多个客户端的请求只需要调用api即可。每个客户端绝大多数是沉默的工作线程只需要等待等待客户端发来请求处理即可。