【大家好,我是爱干饭的猿,本文是计算机网络编程初级入门,主要介绍了网络编程的定义、Socket套接字、UDP数据报套接字编程、TCP流套接字编程、理解协议。
后续会继续分享计算机网络TCP_IP三次握手、四次挥手及其他重要知识点总结,如果喜欢这篇文章,点个赞👍,关注一下吧】
上一篇文章:《【web】计算机网络原理(重点:TCP/IP五层模型)》
🤞目录🤞
DatagramSocket 方法:编辑3.2 DatagramPacket API
🧸1. 网络编程
1.1 什么是网络编程
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。
编辑
当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。
1.2 网络编程中的基本概念
1. 发送端和接收端
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
2. 请求和响应
一般来说,获取一个网络资源,涉及到两次网络数据传输:
第一次:请求数据的发送
第二次:响应数据的发送。
3. 客户端和服务端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端。
对于服务来说,一般是提供:
- 客户端获取服务资源
- 客户端保存资源在服务端
🧸2. Socket套接字
2.1 概念
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
2.2 分类
Socket套接字主要针对传输层协议划分为如下三类:
1. 流套接字
使用传输层TCP协议 TCP,即Transmission Control Protocol(传输控制协议),传输层协议。
特点:
- 有连接
- 可靠传输
- 面向字节流
- 有接收缓冲区,也有发送缓冲区
- 大小不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情 况下,是无边界的数据,可以多次发送,也可以分开多次接收。
2.数据报套接字
使用传输层UDP协议 UDP,即User Datagram Protocol(用户数据报协议),传输层协议。
特点:
- 无连接
- 不可靠传输
- 面向数据报
- 有接收缓冲区,无发送缓冲区
- 大小受限:一次最多传输64k
3. 原始套接字
原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
2.3 Java数据报套接字通信模型
对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数 据报,一次接收全部的数据报。
java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用 DatagramPacket 作为发送或接收的UDP数据报。对于一次发送及接收UDP数据报的流程如下:
编辑
对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:
编辑 2.4 Java流套接字通信模型
编辑
2.5 Socket编程注意事项
1. 客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场 景,一般都是不同主机。
2. 注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程
3. Socket编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议, 也需要考虑,这块我们在后续来说明如何设计应用层协议。
4. 关于端口被占用的问题
检查端口被占用:在cmd输入 netstat -ano | findstr 端口号 ,则可以显示对应进程的pid。对应端口进程的pid 在任务管理器中,通过pid查找进程。
🧸3. UDP数据报套接字编程
3.1 DatagramSocket API
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket 构造方法:
编辑
DatagramSocket 方法:编辑3.2 DatagramPacket API
DatagramPacket 构造方法:
编辑
DatagramPacket 方法:
编辑
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创 建。
3.3 InetSocketAddress API
InetSocketAddress ( SocketAddress 的子类 )构造方法:
编辑
3.4 示例:UDP请求响应
UDP服务端
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; /** * @author haomin * @date 2022/09/26 09:56 **/ public class Client { public static void main(String[] args) throws IOException { // 本机-本机 // 目前服务器在本机: 127.0.0.1 // 目前服务器的端口是: 8080 // 本机只发送一次请求 String word = "apple"; // 准备发送请求 String request = "请求格式头\r\n" + word + "请求格式尾\r\n"; byte[] bytes = request.getBytes("UTF-8"); DatagramPacket sent = new DatagramPacket( bytes, 0, bytes.length, InetAddress.getLoopbackAddress(), 8088 ); DatagramSocket socket = new DatagramSocket(9999); Log.println("发送请求"); socket.send(sent); byte[] buf = new byte[1024]; DatagramPacket received = new DatagramPacket(buf, buf.length); socket.receive(received); int n = received.getLength(); String response = new String(buf, 0, n, "utf-8"); System.out.println(response); } }
UDP客户端
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.util.Arrays; import java.util.HashMap; /** * @author haomin * @date 2022/09/26 09:57 **/ public class Server { // 1. 可以写在文件中 // 2. 可以写在数据库中 private static final HashMap<String, String> map = new HashMap<>(); static { map.put("apple", "苹果"); map.put("banana", "香蕉"); } public static void main(String[] args) throws IOException { DatagramSocket socket = new DatagramSocket(8080); while(true){ // 1. 读取请求 byte[] buf = new byte[1024]; DatagramPacket received = new DatagramPacket(buf, buf.length); socket.receive(received); // 服务器会阻塞在这 // 2. 处理 InetAddress address = received.getAddress(); // 获得ip int port = received.getPort(); // 获得端口 int n = received.getLength(); // 真正收到的数据长度 byte[] actualData = Arrays.copyOf(buf, n); // 真正有用的数据 // byte[] + 字符集编码 -> String String request = new String(actualData, 0, actualData.length, "utf-8"); // 3. 分析请求 if(!request.startsWith("请求格式头\r\n") && !request.startsWith("请求格式尾\r\n")){ continue; // 不符合应用层的请求协议,不做处理 } String englishWord = request.substring("请求格式头\r\n".length(), request.length() - "请求格式尾\r\n".length()); Log.println(englishWord); String chineseWord = map.getOrDefault(englishWord, "不认识"); // 4. 发送响应 String response = String.format("收到,响应为\r\n%s\r\n", chineseWord); // String + 字符集编码 -> byte[] byte[] bytes = response.getBytes("utf-8"); DatagramPacket sent = new DatagramPacket( bytes, 0, bytes.length, address, port ); socket.send(sent); Log.println("响应成功"+response); } } }
Log 日志
import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; /** * @author haomin * @date 2022/09/26 10:30 **/ public class Log { public static void println(Object o){ Date date = new Date(); DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); String now = dateFormat.format(date); System.out.println(now + ": " + o.toString()); } public static void print(Object o){ Date date = new Date(); DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); String now = dateFormat.format(date); System.out.print(now +": "+ o.toString()); } public static void printf(String fmt, Object... args){ Date date = new Date(); DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); String now = dateFormat.format(date); System.out.printf(now + ": " + fmt, args); } public static void main(String[] args) { println("hello word"); print("hello word\r\n"); printf("%s","hello word"); } }
🧸4. TCP流套接字编程
和刚才UDP类似,实现一个简单的英译汉的功能
4.1 ServerSocket API
ServerSocket 是创建TCP服务端Socket的API。
ServerSocket 构造方法:
编辑
ServerSocket 方法:
编辑4.2 Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket 构造方法:
编辑
Socket 方法:
编辑
4.3 TCP中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
长连接:拨通电话->请求->响应->请求->响应->...->请求->响应->挂断电话
短连接:拨通电话->请求->响应->挂断电话->拨通电话->请求->响应->挂断电话...
对比以上长短连接,两者区别如下:
- 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
- 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
- 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
4.4 示例一:TCP请求响应(短连接)
TCP服务端
import java.io.*; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Scanner; /** * @author haomin * @date 2022/09/27 18:33 **/ // UDP: 无连接 面向数据报文-写信(一信一送) // TCP: 有连接 面向字节流 -写信(一堆信一送)-打电话(连接好才行) public class Server { private static final HashMap<String, String> map = new HashMap<>(); static { map.put("apple", "苹果"); map.put("banana", "香蕉"); } public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); while (true){ // 1. 接通连接(电话) —— accept Socket socket = serverSocket.accept(); // 阻塞 InetAddress inetAddress = socket.getInetAddress(); // 得到客户端地址 int port = socket.getPort(); // 得到客户端端口 InputStream is = socket.getInputStream(); // is: 用于读数据 OutputStream os = socket.getOutputStream();// os: 用于写数据 // 2. 读取请求 Scanner scanner = new Scanner(is, "utf-8"); String header = scanner.nextLine(); String englishWord = scanner.nextLine(); // 3. 处理业务 String chineseWord = map.getOrDefault(englishWord, "不认识"); // 4. 发送响应 // 好的\r\n苹果\r\n // OutputStream -> OutputStreamWriter -> PrintWriter OutputStreamWriter writer = new OutputStreamWriter(os, "utf-8"); PrintWriter printWriter = new PrintWriter(writer); printWriter.printf("好的\r\n%s\r\n", chineseWord); printWriter.flush(); socket.close(); Log.println("服务器响应成功"); } } }
TCP客户端
import java.io.*; import java.net.Socket; import java.util.Scanner; /** * @author haomin * @date 2022/09/27 18:33 **/ public class 短连接Client { public static void main(String[] args) throws IOException { while (true) { Socket socket = new Socket("127.0.0.1", 8080); InputStream is = socket.getInputStream(); Scanner scanner = new Scanner(is, "utf-8"); OutputStream os = socket.getOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(os, "utf-8"); PrintWriter printWriter = new PrintWriter(writer); // 发送请求 printWriter.println("我是Java19班的\r\nbanana\r\n"); printWriter.flush(); // 读取响应 String header = scanner.nextLine(); String chineseWord = scanner.nextLine(); System.out.println("响应结果:" + chineseWord); socket.close(); } } }
4.5 示例二:TCP请求响应(长连接)
TCP服务端
import java.io.*; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Scanner; /** * @author haomin * @date 2022/09/27 18:33 **/ // UDP: 无连接 面向数据报文-写信(一信一送) // TCP: 有连接 面向字节流 -写信(一堆信一送)-打电话(连接好才行) public class 长连接Server { private static final HashMap<String, String> map = new HashMap<>(); static { map.put("apple", "苹果"); map.put("banana", "香蕉"); } public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8080); while (true){ // 1. 接通连接(电话) —— accept Socket socket = serverSocket.accept(); // 阻塞 InetAddress inetAddress = socket.getInetAddress(); // 得到客户端地址 int port = socket.getPort(); // 得到客户端端口 InputStream is = socket.getInputStream(); // is: 用于读数据 OutputStream os = socket.getOutputStream();// os: 用于写数据 // OutputStream -> OutputStreamWriter -> PrintWriter OutputStreamWriter writer = new OutputStreamWriter(os, "utf-8"); PrintWriter printWriter = new PrintWriter(writer); // 2. 读取请求 Scanner scanner = new Scanner(is, "utf-8"); while (scanner.hasNextLine()) { String header = scanner.nextLine(); String englishWord = scanner.nextLine(); // 3. 处理业务 String chineseWord = map.getOrDefault(englishWord, "不认识"); // 4. 发送响应 // 好的\r\n苹果\r\n printWriter.printf("好的\r\n%s\r\n", chineseWord); printWriter.flush(); Log.println("服务器响应成功"); } socket.close(); } } }
TCP客户端
import java.io.*; import java.net.Socket; import java.util.Scanner; /** * @author haomin * @date 2022/09/27 18:33 **/ public class 长连接Client { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 8080); for (int i = 0; i < 3; i++) { InputStream is = socket.getInputStream(); Scanner scanner = new Scanner(is, "utf-8"); OutputStream os = socket.getOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(os, "utf-8"); PrintWriter printWriter = new PrintWriter(writer); // 发送请求 printWriter.printf("我是Java19班的\r\napple\r\n"); printWriter.flush(); // 读取响应 String header = scanner.nextLine(); String chineseWord = scanner.nextLine(); System.out.println("响应结果:" + chineseWord); } socket.close(); } }
4.6 示例三:TCP请求响应(多线程+自定义协议)
TCP服务端
import java.io.*; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Scanner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author haomin * @date 2022/09/27 18:33 **/ // UDP: 无连接 面向数据报文-写信(一信一送) // TCP: 有连接 面向字节流 -写信(一堆信一送)-打电话(连接好才行) public class 多线程长连接Server { private static final HashMap<String, String> map = new HashMap<>(); static { map.put("apple", "苹果"); map.put("banana", "香蕉"); } private static class ServerTask implements Runnable{ private final Socket socket; private ServerTask(Socket socket){ this.socket = socket; } @Override public void run() { try { InetAddress inetAddress = socket.getInetAddress(); // 得到客户端地址 int port = socket.getPort(); // 得到客户端端口 InputStream is = socket.getInputStream(); // is: 用于读数据 OutputStream os = socket.getOutputStream();// os: 用于写数据 // OutputStream -> OutputStreamWriter -> PrintWriter OutputStreamWriter writer = new OutputStreamWriter(os, "utf-8"); PrintWriter printWriter = new PrintWriter(writer); // 2. 读取请求 Scanner scanner = new Scanner(is, "utf-8"); while (scanner.hasNextLine()) { String header = scanner.nextLine(); String englishWord = scanner.nextLine(); // 3. 处理业务 String chineseWord = map.getOrDefault(englishWord, "不认识"); // 4. 发送响应 // 好的\r\n苹果\r\n printWriter.printf("好的\r\n%s\r\n", chineseWord); printWriter.flush(); Log.println("服务器响应成功"); } socket.close(); }catch (Exception e){ e.printStackTrace(); } } } public static void main(String[] args) throws IOException { ExecutorService service = Executors.newFixedThreadPool(10); ServerSocket serverSocket = new ServerSocket(8080); while (true){ // 1. 接通连接(电话) —— accept Socket socket = serverSocket.accept(); // 阻塞 ServerTask task = new ServerTask(socket); service.execute(task); } } }
TCP客户端
import java.io.*; import java.net.Socket; import java.util.Scanner; /** * @author haomin * @date 2022/09/27 18:33 **/ public class 长连接子输入Client { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 8080); Scanner systemInSc = new Scanner(System.in); while (systemInSc.hasNextLine()){ String w = systemInSc.nextLine(); InputStream is = socket.getInputStream(); Scanner scanner = new Scanner(is, "utf-8"); OutputStream os = socket.getOutputStream(); OutputStreamWriter writer = new OutputStreamWriter(os, "utf-8"); PrintWriter printWriter = new PrintWriter(writer); // 发送请求 printWriter.printf("我是Java19班的\r\n%s\r\n", w); printWriter.flush(); // 读取响应 String header = scanner.nextLine(); String chineseWord = scanner.nextLine(); System.out.println("响应结果:" + chineseWord); } socket.close(); } }
🧸5. 再谈协议
5.1 理解为什么需要协议
以上我们实现的UDP和TCP数据传输,除了UDP和TCP协议外,程序还存在应用层自定义协议,可以想想分别都是什么样的协议格式。
对于客户端及服务端应用程序来说,请求和响应,需要约定一致的数据格式:
- 客户端发送请求和服务端解析请求要使用相同的数据格式。
- 服务端返回响应和客户端解析响应也要使用相同的数据格式。
- 请求格式和响应格式可以相同,也可以不同。
- 约定相同的数据格式,主要目的是为了让接收端在解析的时候明确如何解析数据中的各个字段。
- 可以使用知名协议(广泛使用的协议格式),如果想自己约定数据格式,就属于自定义协议。
5.2 封装/分用 vs 序列化/反序列化
一般来说,在网络数据传输中,发送端应用程序,发送数据时的数据转换(如java一般就是将对象转换为某种协议格式)
对发送数据时的数据包装动作来说:
- 如果是使用知名协议,这个动作也称为封装
- 如果是使用小众协议(包括自定义协议),这个动作也称为序列化,一般是将程序中的对象转换为特定的数据格式。
接收端应用程序,接收数据时的数据转换,即对接收数据时的数据解析动作来说:
- 如果是使用知名协议,这个动作也称为分用
- 如果是使用小众协议(包括自定义协议),这个动作也称为反序列化,一般是基于接收数据特定的格式,转换为程序中的对象
5.3 如何设计协议
对于协议来说,重点需要约定好如何解析,一般是根据字段的特点来设计协议:
1. 对于定长的字段
可以基于长度约定,如int字段,约定好4个字节即可
2. 对于不定长的字段
- a. 先发送长度+真正的请求[ 18 + ...]
- b. 使用特殊字符区分不同的请求比如:每读到两次\rln 一个请求
分享到此,感谢大家观看!!!
如果你喜欢这篇文章,请点赞加关注吧,或者如果你对文章有什么困惑,可以私信我。
🏓🏓🏓