1.网络初始:
- 局域网(LAN)广域网(WAN)
- IP地址用于定位主机的网络地址。端口号可以标识主机中发送数据接收数据的进程(用于定位主机中的进程)。一个端口只能被一个进程绑定(通常情况),但是一个进程可以绑定多个端口号。
- 协议是俩个人的事情,只有双方都了解并且遵守才有意义!
- 协议分层,上层协议调用下层协议,下层协议给上层协议提供服务,相邻的层是可以相互交互的,但是不能跨层级。
- TCP/IP五层网络模型和封装,假设主机A给主机B发了个helloworld 主机A发送的过程 一.应用层 应用程序会把输入的helloworld构造成约定好的应用层协议的报文 应用程序就会把这个应用数据报文,交给传输层协议 传输层是操作系统内核实现的,操作系统提供了一些API给应用程序, 这些API叫做socket api,代码调用这些api就可以把应用层的数据交给传输层(交给了操作系统内核)二.传输层 传输层这里有很多协议,最典型的就是TCP协议,此处以TCP为例 TCP协议要在之前的基础上,加上个TCP的协议报头 这个TCP报头里面最重要的就是源端口和目的端口! 传输层继续将这个数据交给网络层进行处理 三.网络层 网络层中最典型的就是IP协议 IP协议把整个TCP数据看成整体,作为载荷部分,在前头加上IP协议报头 IP协议报头里面有很多信息,最关键的就是源IP和目的IP 构造好IP数据报之后,IP协议继续把整个数据交给数据链路层 四.数据链路层 数据链路层的协议有很多,最典型的就是以太网 以太网这个歌协议既管数据链路层,又管物理层 以太网数据帧将IP数据报的前头加上帧头(源mac地址和目的mac地址),后头加上帧尾(校验和) 五.物理层 到达物理层的数据已经组织好了 就可以通过物理层设备(网卡)把上述数据的二进制bit流转换成光信号或电信号来传输
- 分用就是封装的逆过程,封装是打包快递,而分用就是拆开快递。
- 网络中的细节太多了,如果一个协议搞定,那这个协议就会非常的复杂。因此就需要拆分,拆分的多了,又要分层。拆分之后,一个协议负责一件事情,这样才把这些关键信息放到了不同的协议报头中。
2.网络编程:
- 网络编程指的是网络上的主机,通过不同的进程,以编程的方式实现网络通信。网络编程主要是针对应用层。
- 网络编程套接字,就是研究如何写代码完成网络编程。socket api是一切网络编程的基础。
- socket套接字是操作系统给应用程序提供的API,描述的是应用层和传输层的交互,其实socket api就是传输层给应用层提供的。
- API就是一组类和方法。应用程序就可以通过socket api来进行网络编程(操作网卡)。
- 网络传输层中又很多种协议,最主要的就是TCP和UDP。因此操作系统就提供了俩个不同的版本的API。
- 传输层中TCP和UDP的区别
TCP | UDP |
有连接 | 无连接 |
可靠传输 | 不可靠传输 |
面向字节流 | 面向数据报 |
全双工 | 全双工 |
- TCP和文件操作一样是基于“流”的,而UDP则是以“数据报”为基本单位。全双工是一个通道,双向通信;半双工是一个通道,单向通信。网络通信一般都是全双工的。有连接就相当于打电话,必须通信的双方建立好了连接才可以正常打电话;而无连接相当于发短信,直接就可以发送过去。
3.UDP数据报套接字:
- DatagramSocket、DatagramPacket是UDP socket需要掌握的类。
- DatagramSocket,是网卡的代言人,借助这个类来读写网卡。通过网卡发送数据就是写文件,接收数据就是读文件。
方法签名 | 说明 |
DatagramSocket() | 一般用于客户端,创建一个UDP数据报套接字的socket,绑定到随机一个端口 |
DatagramSocket(int port) | 一般用于服务器,创建一个UDP数据报套接字的socket,绑定到指定端口 |
void receive(DatagramPacket p ) | 接收数据报,没有收到会阻塞等待 |
void send(DatagramPacket p ) | 发送数据报,不会阻塞等待 |
void close() | 关闭数据报套接字 |
- socket本质上是一个文件。socket对应到网卡这个硬件设备,操作系统也是把网卡当作文件来管理。通过网卡发送数据,就是写文件;通过网卡接收数据,就是读文件。
- DatagramPacket,代表一个UDP数据包,是一次发送/接受的基本单位;发送和接收的是DatagramPacket。
方法签名 | 说明 |
DatagramPacket(byte[ ] b,int length) | 构造一个DatagramPacket用来接收数据报,接收的数据保存在字节数组里 |
DatagramPacket(byte[ ] b,int offset,int length,address) | 构造一个DatagramPacket用来发送数据报,发送的数据为字节数组的指定长度。address为指定目的主机的IP和端口号。 |
getAddress() | 从接收的数据报中获取发送端主机IP地址;或从发送的数据报中获取接收端主机IP地址 |
int getPort() | 从接收的数据报中获取发送端主机端口号;或从发送的数据报中获取接收端主机端口号 |
byte[ ] getData() | 获取数据报中的数据 |
- UDP实现回显服务器(服务器部分)
//UDP版本:回显服务器的服务器部分 public class UdpEchoServer { private DatagramSocket socket = null; //参数的端口表示服务器要绑定的端口 //不需要指定IP,就是本机的IP public UdpEchoServer(int port) throws SocketException { socket = new DatagramSocket(port); } //启动服务器 public void start() throws IOException { System.out.println("服务器启动了!!!"); while(true){ //1.读取请求并且解析 //socket的receive操作需要一个空的requestPacket,receive方法的参数是一个输出型参数 //将空的DatagramPacket对象交给receive,在receive里面负责把从网卡读到的数据填充到这个对象中 DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//要给DatagramPacket申请内存空间 socket.receive(requestPacket); //将DatagramPacket转换成字符串 getData()是获取数据报中的数据,返回一个byte[] String request = new String(requestPacket.getData(),0, requestPacket.getLength()); //2.根据请求计算响应 String response = process(request); //3.把响应返回给客户端 //发送DatagramPacket对象需要指定IP地址和端口号 DatagramPacket responsePacket = new DatagramPacket( //getSocketAddress就是得到客户端的IP和端口号 response.getBytes(),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); } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { UdpEchoServer server = new UdpEchoServer(1025); server.start(); } }
- UDP实现回显服务器(客户端部分)
//UDP版本:回显服务器的客户端部分 public class UdpEchoClient { private DatagramSocket socket = null; private String serverIP; private int serverPort; //服务器的IP一般不用写,就是本机的IP //需要传服务器的IP和服务器的端口 public UdpEchoClient(String serverIp,int serverPort) throws SocketException { socket = new DatagramSocket();//不用指定参数 this.serverIP = serverIp; this.serverPort = serverPort; } public void start() throws IOException { Scanner scanner = new Scanner(System.in); while(true){ //1.从控制台上读取用户输入的内容 System.out.print("-> "); String request = scanner.nextLine(); //2.构造一个UDP请求,发送给服务器 DatagramPacket requestPacket = new DatagramPacket( //request.getBytes().length这里的length单位是字节 //request.length()这里的length()单位是字符,不可以改成这样 request.getBytes(),request.getBytes().length, InetAddress.getByName(this.serverIP),this.serverPort); socket.send(requestPacket); //3.从服务器接收响应,并且转成字符串 DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024); 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 { //IP是某某食堂,端口号是某某窗口 UdpEchoClient client = new UdpEchoClient("127.0.0.1",1025); client.start(); } }
- DatagramPacket的三种构造方法
- 为什么服务器需要指定端口,而客户端不用指定端口???答:服务器指定端口目的就是方便客户端找到服务器在哪,而客户端不指定端口因为操作系统会分配一个空闲的端口,如果手动指定了万一用户电脑上的其他程序占用了这个端口,就会导致程序无法正确运行了。
- 对于服务器来说,读取请求并且解析、根据请求计算响应和把响应写回到客户端执行的速度是极快的。如果有多个客户端同时发来请求,服务器也是可以响应的,但是本质上这三个请求的串行处理的!
- 当前俩个程序放在同一个主机上是通过127.0.0.1这个IP来通信的;也可以把俩个程序放在不同的主机上也是可以通信的,但是如果放在不同的主机上,要确保服务器的地址是可以访问到的!
4.TCP流套接字:
- ServerSocket、Socket是TCP流套接字需要掌握的俩个类。
- ServerSocket是创建TCP服务器的api,给服务器使用的类,用来监听端口。
方法签名 | 说明 |
ServerSocket(int port) | 创建一个服务端流套接字Socket,并且绑定指定端口 |
Socket accept() | 有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
void close() | 关闭此套接字 |
- Socket既会给服务器使用也会给客户端使用,用来传输数据。
方法签名 | 说明 |
Socket(String host,int port) | 创建一个客户端流套接字Socket,并且尝试和对应IP的主机上对应端口的进程建立连接 |
InetAddress getInetAddress() | 返回套接字所连接的地址,获取IP和端口 |
int getPort() | 返回套接字所连接的端口 |
InputStream getInputStream() | 返回此套接字的输入流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
- TCP实现回显服务器(服务器部分)
//TCP版本:回显服务器服务器部分 public class TcpEchoServer { private ServerSocket listenSocket = null; public TcpEchoServer(int port) throws IOException { listenSocket = new ServerSocket(port); } public void start() throws IOException { System.out.println("服务器启动!!"); //使用线程池 ExecutorService service = Executors.newCachedThreadPool(); while(true) { //1.先调用accept Socket clientSocket = listenSocket.accept(); //2.再来处理这个连接,这里应该使用多线程,每个客户端连上来都分配一个新的线程负责处理 //使用多线程确实可以解决问题,但是会频繁的创建和销毁线程! // 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) { e.printStackTrace(); } } }); } } private void processConnection(Socket clientSocket) throws IOException { System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(),clientSocket.getPort()); //处理客户端的请求 //clientSocket代表的是服务器的网卡,inputStream代表从网卡读数据也就相当于从客户端读取数据 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; } String request = scanner.next(); //2.根据请求计算响应 String response = process(request); //3.把响应写回到客户端 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(response); //刷新缓冲区确保数据确实是通过网卡发送出去了 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 { //为什么这个clientSocket要关闭文件,前面的listenSocket和UDP程序中的socket都不需要关闭文件呢? clientSocket.close(); } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9090); server.start(); } }
- TCP实现回显服务器(客户端部分)
//TCP版本:回显服务器客户端部分 public class TcpEchoClient { private Socket socket = null; public TcpEchoClient(String serverIp, int serverPort) throws IOException { socket = new Socket(serverIp, serverPort); } public void start() { Scanner scanner = new Scanner(System.in); try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { while (true) { //1.从控制台读取数据 System.out.print("-> "); 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.println(response); } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { TcpEchoClient server = new TcpEchoClient("127.0.0.1",9090); server.start(); } }
- TCP实现回显服务器的服务器代码部分,如果使用1的话,就不能让多个客户端同时使用服务器;如果使用2,频繁的创建和销毁代价较大,所以推荐使用线程池!
- TCP实现回显服务器的服务器代码中需要close文件!
- 启动服务器,如果没有客户端建立连接,服务器就会阻塞等待。如果有一个客户端过来了,此时就会显示客户端已上线并且向下执行代码。如果再有一个客户端也过来了,使用上述的方案1那么不会显示这个客户端已上线。
- TCP中的长短连接。TCP发送数据时需要先建立连接,什么时候关闭连接就决定是短连接还是长连接。短连接:每次收到数据并返回响应后,都关闭连接,也就是短连接只能一次收发数据,TCP每个连接只处理一个客户端请求能够保证快速调用到accept。长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,也就是长连接可以多次收发数据,TCP建立连接之后,要处理客户端的多次请求才导致无法快速调用accept。
如果对您有帮助的话,
不要忘记点赞+关注哦,蟹蟹
如果对您有帮助的话,
不要忘记点赞+关注哦,蟹蟹
如果对您有帮助的话,
不要忘记点赞+关注哦,蟹蟹