当前位置: 首页 > news >正文

网络编程

目录

TCP 和 UDP 的特点

有连接 vs 无连接

可靠传输 vs 不可靠传输

面向字节流 vs 面向数据报

全双工 vs 半双工

Socket 套接字

流套接字

数据报套接字

原始套接字

UDP 数据报套接字编程

DatagramSocket

DatagramPacket

实现简易回显服务器

TCP流套接字编程

ServerSocket

Socket

实现简易回显服务器


TCP 和 UDP 的特点

传输层两个核心协议的特点:

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

UDP:无连接,不可靠传输,面向数据报,全双工

有连接 vs 无连接

抽象的概念,是虚拟的/逻辑上的连接

TCP协议中保存了对端的信息,就是有连接。A 和 B 通信,A 和 B 先建立连接,让 A 保存 B 的信息,B 保存 A 的信息(彼此之间知道,谁是和他建立连接的那个)

对于UDP来说,UDP协议本身不保存对方的信息,就是无连接

可靠传输 vs 不可靠传输

网络上,由于光信号/电信号都可能受到外界的干扰,导致数据非常容易出现丢失的情况(丢包)。比如说,本来是传输0101,其中有些 bit 位被修改了,这样的数据就会被识别出来,并且把它给丢弃掉,这就是丢包

可靠传输:尽可能的提高传输成功的概率,而不是保证数据包100%到达。如果出现丢包了,能够感知到

TCP是可靠传输,内部的机制会对丢包的情况进行处理

不可靠传输:把数据传输出去就不管了

直观感觉可靠传输更好,但可靠传输也是要付出代价的,效率不如不可靠传输

面向字节流 vs 面向数据报

面向字节流,读写数据的时候,是以字节为单位

TCP中读写数据完全按照字节流向的方式,可以任意的读取或者是写若干个字节都很灵活

优点是支持任意长度,但是会有粘包问题

面向数据报,读写数据的时候,以一个数据报为单位(不是字符)

UDP一次必须读写一个UDP数据报,不能是半个

不存在粘包,但是会有长度限制

全双工 vs 半双工

全双工:一个通信链路,支持双向通信(能读,也能写)

半双工:一个通信链路,只支持单向通信(要么读,要么写)

Socket 套接字

Socket 套接字(套接字就是 API),是由操作系统提供用于网络通信的 API,是基于TCP/IP协议实现的,主要作用于传输层,因此程序员只需制定好应用层的协议,再调用此 API,即可实现网络通信。基于 Socket 套接字的网络程序开发就是网络编程

socket:插座,插槽(主板上的一些接口)

Socket 套接字 / Socket api:是操作系统提供的一组 api,是传输层给应用层提供的

TCP 和 UDP 的差别非常大,编写代码的时候,也是不同的风格。因此,提供了几套不同的 socket api;Socket 套接字主要针对传输层协议划分为如下三类:

流套接字

使用传输层 TCP 协议

TCP:即 Transmission Control Protocol(传输控制协议),传输层协议

数据报套接字

使用传输层 UDP 协议

UDP:即 User Datagram Protocol(用户数据报协议),传输层协议

原始套接字

原始套接字用于自定义传输层协议,用于读写内核没有处理的 IP 协议数据(了解)

UDP 数据报套接字编程

DatagramSocket

是 UDP 的 Socket,用于发送和接收 UDP 数据报,通过不同的构造方法,可以指定作为客户端还是服务端,客户端一般不需要指定端口号,由操作系统分配,而服务端的端口号则需程序员指定,固定的端口号可以更方便地为客户端服务

1. 计算机中的 “文件" 通常是一个 “广义的概念”。文件也能代指一些硬件设备(操作系统管理硬件设备,也是抽象成文件统一管理的)

2. 网卡在系统中就被抽象成了 socket 文件,操作网卡的流程和操作普通文件差不多,都是打开  -> 读写 -> 关闭;打开文件时,也会在文件描述符表中分配一个表项

3. 网卡直接操作不好操作,所以把操作网卡转换成操作 socket 文件,socket 文件相当于 "网卡的遥控器"

4. DatagramSocket 就是用来表示网卡的文件,通过它来操作网卡

构造方法:可以通过构造方法打开文件

方法签名方法说明
DatagramSocket()创建一个UDP数据报套接字的 Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port)创建一个UDP数据报套接字的 Socket,绑定到本机指定的端口(一般用于服务端);port:端口号

方法:

方法签名方法说明
void receive(DatagramPacket p)从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p)从此套接字发送数据报包(不会阻塞等待,直接发送)
void close()关闭此数据报套接字

receive(DatagramPacket p):输出型参数,把参数作为 “输出的结果”

调用之前,先构造一个空的对象(不是null),把对象传递到 receive 里,receive 就会把数据从网卡读出来,填充到参数中

DatagramPacket

用于封装一个完整的 UDP 数据报

Java 中使用 UDP 协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用DatagramPacket 作为发送或接收的 UDP 数据报

构造方法:可以通过构造方法来指定 UDP 数据包的载荷数据

方法签名方法说明
DatagramPacket(byte[] buf, int length)构造一个 DatagramPacket 以用来接收数据报,接收的数据保存在字节数组(第一个参数 buf)中,接收指定长度(第二个参数 length
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)构造一个 DatagramPacket 以用来发送数据报,发送的数据为字节数组(第一个参数 buf)中,从指定偏移量 offset 开始,长度为 length 的数据,address 指定目的主机的 IP 和端口号

方法:

方法签名方法说明
InetAddress getAddress()从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
int getPort()从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData()获取数据报中的数据

实现简易回显服务器

实现一个简易回显服务器,将客户端发送的数据原样返回

// 服务器
public class UdpEchoServer {private 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 表示一个 UDP 数据报. 此处传入的字节数组, 就保存 UDP 的载荷部分.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. 把响应返回给客户端//    根据 response 构造 DatagramPacket, 发送给客户端.//    此处不能使用 response.length()DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress());//    此处还不能直接发送. UDP 协议自身没有保存对方的信息(不知道发给谁)//    需要指定 目的 ip 和 目的端口.socket.send(responsePacket);// 4. 打印一个日志System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),request, response);}}// 后续如果要写别的服务器, 只修改这个地方就好了.// private 方法不能被重写. 想要被重写需要把 private 改成 publicpublic String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer server = new UdpEchoServer(9090);server.start();}
}

说明:

1. Echo:回声;EchoServer(回显服务器):将客户端发送的数据原样返回

2. 对于服务器来说,客户端什么时候发请求,发多少个请求,无法预测。因此服务器中通常都需要有一个死循环,持续不断的尝试读取客户端的请求数据(7*24小时运行)

3. RequestPacket 虽然表示 UDP 数据包,但它不只包含源端囗/目的端囗,也包含了源IP/目的 IP,其中目的端囗和目的 IP 都是客户端发送过来的

4. 通过 requestPacket.getData() 拿到 DatagramPacket 中的字节数组,getLength() 拿到有效数据的长度

5. response.getBytes():拿到字符串中的字节数组

response.getBytes().length:拿到字节数组的长度,这里不能使用字符串长度(单位是字符)

requestPacket.getSocketAddress():这个方法返回的对象中同时包含IP和端口,可以拿到客户端的IP和端口号

6. 这里 new DatagramPacket 是要构造响应数据报

7. 这里的 socket 不用 close。文件是否要关闭,要考虑清楚这个文件对象的生命周期是怎样的,此处的 socket 对象,伴随整个udp 服务器自始至终,如果服务器关闭(进程结束),进程结束时就会自动释放 PCB 的文件描述符表中的所有资源,所以就不需要手动调用 close 了

8. 在客户端发送请求过来之前,服务器里面的逻辑都在干啥呢?

receive 会触发阻塞行为。客户端请求发来了,receive 才会返回,客户端的请求没来,receive 就一直阻塞了

// 客户端
public class UdpEchoClient {private DatagramSocket socket = null;// UDP 本身不保存对端的信息, 就自己的代码中保存一下private String serverIp;private int serverPort;// 和服务器不同, 此处的构造方法是要指定访问的服务器的地址.public UdpEchoClient(String serverIp, int serverPort) throws SocketException {this.serverIp = serverIp;this.serverPort = serverPort;socket = new DatagramSocket();}public void start() throws IOException {Scanner scanner = new Scanner(System.in);while (true) {// 1. 从控制台读取用户输入的内容.System.out.println("请输入要发送的内容:");if (!scanner.hasNext()) {break;}String request = scanner.next();// 2. 把请求发送给服务器, 需要构造 DatagramPacket 对象.//    构造过程中, 不光要构造载荷, 还要设置服务器的 IP 和端口号DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,InetAddress.getByName(serverIp), serverPort);// 3. 发送数据报socket.send(requestPacket);// 4. 接收服务器的响应DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);// 5. 从服务器读取的数据进行解析, 打印出来.String response = new String(responsePacket.getData(), 0, responsePacket.getLength());System.out.println(response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);client.start();}
}

说明:

1. DatagramSocket构造方法必须要使用无参数版本的。因为如果客户端是固定端口,很可能客户端运行的时候这个端口被别的程序占用,就会使当前程序运行失败

2. serverlp 是按字符串的方式构造的,例如192.168.100.152 这种形式的字符串。RequestPacket 没有参数是字符串(目的IP)的构造方法,所以需要用 InetAddress.getByName 方法进行转化

3. 在构造 client 对象时,需要指定服务器的 IP 和端口。127.0.0.1 是特殊的 IP,称为环回 IP,表示当前这个主机。无论主机的真实 IP 是啥,都可以使用 127.0.0.1 代替(类似于 this)。由于此时,客户端和服务器在同一个主机上,就可以使用 127.0.0.1 来访问,如果是不同主机,就需要填写其他的 IP 了

Socket编程注意事项:

1. 客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景,一般都是不同主机。

2. 注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程

3. Socket 编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议,也需要考虑,这块我们在后续来说明如何设计应用层协议。

4. 关于端口被占用的问题

5. 如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。

基于上述回显服务器实现翻译服务器(中译英)

public class UdpDictServer extends UdpEchoServer {private HashMap<String, String> dict = new HashMap<>();public UdpDictServer(int port) throws SocketException {super(port);// 初始化词典dict.put("小猫", "cat");dict.put("小狗", "dog");dict.put("小兔子", "rabbit");dict.put("小鸭子", "duck");}@Overridepublic String process(String request) {// 查字典.return dict.getOrDefault(request, "未找到该词条");}public static void main(String[] args) throws IOException {UdpDictServer server = new UdpDictServer(9090);server.start();}
}

TCP流套接字编程

ServerSocket

是创建TCP服务端Socket的API(专门给服务器用的)

构造方法:

方法签名方法说明
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口

方法:

方法签名方法说明
Socket accept()开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
void close()关闭此套接字

Socket

是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket(服务器和客户端都会用)

构造方法:

方法签名

方法说明
Socket(String host, int port)创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接(两个参数是服务器的IP和服务器的端囗)

方法:

方法签名方法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输入流
OutputStream getOutputStream()返回此套接字的输出流

实现简易回显服务器

public class TcpEchoServer {private ServerSocket serverSocket = null;// 这里和 UDP 服务器类似, 也是在构造对象的时候, 绑定端口号.public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("启动服务器");// 这种情况一般不会使用 fixedThreadPool, 意味着同时处理的客户端连接数目就固定了.ExecutorService executorService = Executors.newCachedThreadPool();while (true) {// tcp 来说, 需要先处理客户端发来的连接.// 通过读写 clientSocket, 和客户端进行通信.// 如果没有客户端发起连接, 此时 accept 就会阻塞.// 主线程负责进行 accept, 每次 accept 到一个客户端, 就创建一个线程, 由新线程负责处理客户端的请求.Socket clientSocket = serverSocket.accept();// 使用多线程的方式来调整
//            Thread t = new Thread(() -> {
//                processConnection(clientSocket);
//            });
//            t.start();// 使用线程池来调整executorService.submit(() -> {processConnection(clientSocket);});}}// 处理一个客户端的连接.// 可能要涉及到多个客户端的请求和响应.private void processConnection(Socket clientSocket) {System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());try (InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {// 针对 InputStream 套了一层Scanner scanner = new Scanner(inputStream);// 针对 OutputStream 套了一层PrintWriter writer = new PrintWriter(outputStream);// 分成三个步骤while (true) {// 1. 读取请求并解析. 可以直接 read, 也可以借助 Scanner 来辅助完成.if (!scanner.hasNext()) {// 连接断开了System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());break;}String request = scanner.next();// 2. 根据请求计算响应String response = process(request);// 3. 返回响应到客户端// outputStream.write(response.getBytes());writer.println(response);writer.flush();// 打印日志System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(),request, response);}} catch (IOException e) {throw new RuntimeException(e);} finally {try {clientSocket.close();} catch (IOException e) {throw new RuntimeException(e);}}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(9090);server.start();}
}
public class TcpEchoClient {private Socket socket = null;public TcpEchoClient(String serverIp, int serverPort) throws IOException {// 可以直接把字符串的 IP 地址, 设置进来,比 UDP 的写法更方法// 127.0.0.1 这种字符串socket = new Socket(serverIp, serverPort);}public void start() {Scanner scanner = new Scanner(System.in);try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {// 为了使用方便, 套壳操作Scanner scannerNet = new Scanner(inputStream);PrintWriter writer = new PrintWriter(outputStream);// 从控制台读取请求, 发送给服务器.while (true) {// 1. 从控制台读取用户输入String request = scanner.next();// 2. 发送给服务器writer.println(request);//    加上刷新缓冲区操作, 才是真正发送数据writer.flush();// 3. 读取服务器返回的响应.String response = scannerNet.next();// 4. 打印到控制台System.out.println(response);}} catch (IOException e) {throw new RuntimeException(e);}}public static void main(String[] args) throws IOException {TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);client.start();}
}

说明:

1. Scanner 不仅可以处理控制台上的输入,也可以处理文件的输入,还能控制网络的输入。Scanner 构造方法,填入的其实是一个 InputStream 对象

底层:

2. 创建 socket 对象就会在底层和对端建立 tcp 的连接(记录对端的信息),服务器的 ip 和端口不需要自己创建变量保存了,直接 tcp 内部就记住了

3. 服务器和客户端的 socket 对象,绝对不是同一个对象(分别在不同进程中,甚至在不同主机上)。但是二者存在密切的关联,可以理解成两部手机进行通话

4. 这个操作只是把数据放到 “发送缓冲区” (内存空间)中,还没有真正写入到网卡里,加上刷新缓冲区操作, 才是真正发送数据。这是 PrintWriter 的行为,如果不套壳,是可以直接发送的。但是实际开发中会广泛使用缓冲区,所以 flush 这个操作是很关键的

5. 上图的 println 约定了,一个请求/响应使用 \n 作为结束标记,对端读的时候,也是读到 \n就结束(认为是读到一个完整的请求了)

6. 每个客户端连接,都会创建一个新的 clientSocket,每个客户端断开连接,这个对象也就可以不要了,所以需要手动释放

7. TCP服务器使用多线程的原因:单线程服务器在处理客户端请求时,需按顺序依次完成当前连接的服务后才响应新连接,导致后续客户端长时间等待。多线程模式可为每个连接独立创建线程,实现并行处理多个客户端请求,显著提高服务器吞吐量

http://www.dtcms.com/a/471267.html

相关文章:

  • 做调研用到的大数据网站长沙制作网站软件
  • 网站设计培训学校找哪家成都网站搜索排名优化公司
  • Python中joblib库并行求解多个方程组
  • 购买空间后怎么上传网站越南外贸平台
  • c 网站建设教程视频建筑模拟器2022下载
  • C语言--核心语法
  • 网站建设兼职合同模板门户网站是什么
  • 基于SpringBoot的高校(学生综合)服务平台的设计与实现
  • 模板自助建站网站备案信息不准确
  • SAP MM 通用物料移动过账接口分享
  • 【ThinkPHP6系列学习-5】获取变量
  • AI技术路线之争
  • Android编译插桩ASM技术探究(一)
  • 西安网站建设软件模板下载失败
  • 学校的网站开发过程钓鱼软件生成器
  • 宁波企业建站网站建设科技
  • 网站开发学习什么网站建设实施规范
  • 安徽建设厅网站进不去郑州网站优化公司平台
  • 如何在网站投放广告wordpress标题后乱码
  • 【C++学习】继承和多态
  • 开发一个网站需要多少人杭州品牌vi设计公司
  • 韩雪冬做网站多少钱网址搜索ip地址
  • Google 智能体设计模式:探索与发现
  • 湛江购房网官方网站沈阳点金网站建设
  • 靖江网站设计做网站服务好
  • 制作网站的素材wordpress怎么改表缀
  • 合肥网站制作公司有哪些公司网站维护中 源码
  • C++的string类
  • 【软件设计师中级】计算机组成与结构(五):指令系统与计算机体系结构 - CPU的“思维语言“与架构蓝图
  • 柳州网站建设数公式大全wordpress 输出the id