写在前
简单描述一下关于路由器的三层转发和交换机的二层转发。
路由器是三层转发-->在网络层转发。【需要解析出IP协议中的源IP、目的IP来规划路径】
交换机是二层转发-->在数据链路层转发。【只需要关注下一步发展到哪个相邻的设备上,不需要IP地址,在数据链路层。有一个mac地址。】
这一部分开始进行网络部分的内容,涉及到编程相关的知识,好好学习!!
本篇博客主要是对网络编程进行初步的了解,socket的理解,学习相关API、完成客户端、服务器的【回显服务器】代码编写,在同一台主机上完成通信。
目录
写在前
1.网络编程基础
1.1什么是网络编程
1.2常见的客户端服务端模型
2.Socket套接字
2.1概念
2.2分类
2.2.1流套接字-TCP
2.2.2数据报套接字-UDP
2.3Java数据报套接字通信模型
2.4UDP数据报套接字编程
2.4.1DatagramSocket API
2.4.2DatagramPacket API
2.4.3InetSocketAddress API
2.4.4服务器代码实现
核心思想:
代码实现:
加注释版本:
需要强调的内容:
2.4.5客户端代码实现
核心思想:
代码实现:
加注释版本:
理解服务器与客户端端口:
2.4.6服务器和客户端执行顺序:
2.4.7IDEA多个客户端设置操作
2.4.8客户端服务器执行结果
1.网络编程基础
1.1什么是网络编程
网络编程,指网络上的主机,通过不同的进程,【只要满足不同的进程就行,所以即便是同一个主机,只要是进程不同,基于网络来传输数据,也属于网络编程。】以编程的方式实现网络通信(或称为网络数据传输。)
1.2常见的客户端服务端模型
最常见的场景,客户端是指给用户使用的程序,服务端是指提供用户服务的程序。
(1)客户端先发送请求到服务端。
(2)服务端根据请求数据,执行相应的业务处理。
(3)服务端返回响应,发送业务处理结果。
(4)客户端根据响应数据,展示处理结果(战术获取的资源,或提示保存资源的处理结果)
2.Socket套接字
2.1概念
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本单元。基于Socket套接字的网络程序开发就是网络编程。
【socket 英文原意为:插座。socket是操作系统给应用程序提供的网络编程的API,可以认为socket API是和传输层密切相关的,传输层里提供了两个核心的协议:TCP和UDP。所以socket api提供了两种风格TCP和UDP 准确的讲有三种unix域套接字,几乎没人用】
2.2分类
一些解释:
可靠传输:网络环境是天然复杂的。不可能保证传输的百分百到达,可靠传输指的是发送方能知道自己的消息发过去了。
是否是可靠传输与是否有无连接没有关系:例如在一些聊天软件上的“已读”功能,是无连接的可靠传输。
全双工半双工:一个通信通道可双向传输(既可以发送也可以接收),就是全双工;一根水管只能单项传输,叫做半双工。TCP/UDP都是全双工是因为一根网线中有八根线
2.2.1流套接字-TCP
TCP,Transmission Control Protocol(传输控制协议),传输层协议。
特点:有连接,可靠传输,面对字节流,有接收缓冲区也有发送缓冲区,大小不限,全双工。
类似于打电话,打电话就是有连接的,需要连接建立才能通信。连接建立需要对方“接受”,如果连接没建立好,则不能进行通信。
对于字节流来说,可以简单地理解为,传输数据是基于IO流的,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
2.2.2数据报套接字-UDP
UDP,User Datagram Protocol(用户数据报协议),传输层协议。
特点:无连接,不可靠传输,面向数据报,有接受缓冲区无发送缓冲区,大小受限:一次最多传输64K,全双工。
类似于发微信,
对于数据报来说,可以简单理解为,传输数据是一块一块的,发送一块数据假如有100个字节,必须一次发送,接收的也必须是100个字节,而不能分100次,每次接收一个字节。
拓展:关于网速快慢
“木桶原理” 在网络通信的过程中有很多路由器、交换机。转发能力存在上限,转发的不止是一个用户的数据。某个设备达到转发能力的上限,就会卡顿丢包。
2.3Java数据报套接字通信模型
对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次会发送全部数据报。一次接收全部数据报。
Java中使用UDP协议通信,主要基于DatagramSocket类来创建数据报套接字,并使用DatagramPacket作为发送或接收的UDP数据报,对于一次发送及接收UDP数据报的流程如下:
以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据,也就是只有请求,没有响应。对于一个服务器来说,重要的是提供多个客户端的请求及响应。
下面实现一个最简单的UDP版本的客户端服务器程序--回显服务器(echo serve)
2.4UDP数据报套接字编程
一个普通的服务器:收到请求,根据请求计算响应,返回响应。
echo serve:省略了其中的”根据请求计算响应“,请求是啥,就返回啥(这个代码没有实际的业务,也没有太大作用和意义,只是为了展示socket api 基本用法),但是对于一个服务器来说,一定”根据请求计算响应“这个环节是最重要的。
2.4.1DatagramSocket API
使用这个类表示一个socket对象。【在操作系统中,把socket对象也是当作一个文件来处理的,相当于是文件描述符表中的一项】普通文件对应硬盘设备;socket文件对应网卡。
1个socket对象就可以和另外一台主机进行通信,如果需要多个不同主机通信,则需要创建多个socket对象。
DatagramScocket是UDP Socket,用于发送和接收UDP数据报。
socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄。
数据报(Data gram),通过网络传输的数据的基本单元,包含一个报头(header)和数据本身,其中报头描述了数据的目的地以及和其它数据之间的关系。 完备的、独立的数据实体,该实体携带要从源计算机传递到目的计算机的信息,该信息不依赖以前在源计算机和目的计算机以及传输网络间交换。
方法签名 | 方法说明 |
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务器) |
方法签名 | 方法说明 |
void receive(DatagramPacket p) 此处传入的相当于是一个空对象,receive方法内部,会对参数的这个空对象进行内容填充,从而构造出结果数据,参数也是一个”输出型参数“ | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(Datagram Packet p) | 从此套接字发送数据报(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
2.4.2DatagramPacket API
DatagramPacket是UDP Socket发送和接收的数据报,表示UDP中传输的一个报文。
方法签名 | 方法说明 |
DatagramPacket(byte buf ,int length) 把buf这个缓冲区给设置进去了 | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length) |
DatagramPacket(byte[ ] buf,int offset,int length,SocketAddress address) Socket Address Ip端口号 | 构造一个DatagramSocketPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号。 |
方法签名 | 方法说明 |
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址。 |
int getPort() | 从接收端的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号。 |
byte[ ] getData() | 获取数据报中的数据 |
构造UDP发送的数据时,需要传入socketAddress,该对象可以使用InetSocketAddress来创建。
2.4.3InetSocketAddress API
方法签名 | 方法说明 |
InetSocketAddress(InetAddress addr,int port) | 创建一个Socket地址,包含IP地址和端口号。 |
2.4.4服务器代码实现
核心思想:
(1)读取请求并解析
(2)根据请求计算响应
(3)构造响应并写回给客户端
数据到达网卡,经过内核的层层分用,最终到达UDP传输层协议。
调用receive相当于执行到内核中的相关udp代码,就会把这个udp数据报里面载荷部分取出来,拷贝到用户提供的byte[ ]数组中。
代码实现:
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//UDP版本的回显服务器
public class UdpEcSe {
private DatagramSocket socket = null;
public UdpEcSe(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);//通过receive方法来读取请求,receive内部会针对参数对象填充数据。填充的数据来自于网卡
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//2.根据请求计算响应,此处是一个回显服务器,所以请求和响应是相同的
String response = process(request);
//3.将响应写回客户端,需要用到send方法
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(requestPacket);
//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 {
UdpEcSe server = new UdpEcSe(9090);
server.start();
}
}
加注释版本:
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 苏西西
* Date: 2023-10-18
* Time: 14:59
*/
//UDP版本的回显服务器
public class UdpEcSe {
//首先需要创建一个成员
//网络编程首先是要操作网卡,网卡不方便直接操作
// 在操作系统内核中使用了一种特殊的叫做“socket”这样的文件来抽象表示网卡
//因此进行网络通信需要先有一个对象
private DatagramSocket socket = null;
//构造方法将对象实例化
//对于服务器来说,创建的同时还需要关联上一个端口号
//服务器是网络传输中被动的一方,如果是操作系统随机分配的端口,
public UdpEcSe(int port) throws SocketException {
socket = new DatagramSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
//服务器不是只给一个客户端提供服务,需要去服务很多客户端
while (true){
//只要有客户端过来就可以提供服务
//1.读取客户端发来的请求是啥
//对于UDP来说,传输数据的基本单位是DatagramPacket
//receive方法的参数是一个输出型参数,需要先构造好一个空白的DatagramPacket对象交给receive 来进行填充
//DatagramPacket相当于是在点餐的时候给顾客一个空白的纸条,客户写完之后商家再根据纸条的需求来做食物
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);//通过receive方法来读取请求,receive内部会针对参数对象填充数据。填充的数据来自于网卡
//此时这个DatagramPacket 是一个特殊的对象,并不方便直接进行处理
//可以将这里包含的数据拿出来构造一个字符串
//字节数组作为存储数据的缓冲区,此处给的最大的长度是4096,但是这里的空间不一定会被用满,可能只使用小部分
//因此构造字符串,哪些用了构造那部分,就可以通过getLength获取到实际的数据报的长度
//只把这个实际有效部分给构造成字符串即可
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//2.根据请求计算响应,此处是一个回显服务器,所以请求和响应是相同的
String response = process(request);
//3.将响应写回客户端,需要用到send方法
// send的参数也是一个DatagramPacket,需要将这个对象构造好
//这里也需要通过字节数组进行构造,不同的是读请求是需要构造一个空的字节数组
//读响应的字节数组就不能是空的了,得带有respose里面的内容--响应内容
//需要注意的是response.getBytes().length是计算字节的个数,这里不能写作response.length() 【字符的个数】
//DatagramPacket只认字节不认字符。
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(requestPacket);
//4.打印一下,当前这次请求响应的处理中间结果
System.out.printf("[%s:%d] req: %s;resp:%s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//根据请求计算响应
//如果不是回显服务器,则可以在process中灵活的进行处理!!
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
//端口号指定,可以在1024->65535范围内随便挑一个数字!
UdpEcSe server = new UdpEcSe(9090);
server.start();
}
}
需要强调的内容:
2.4.5客户端代码实现
核心思想:
(1)从控制台读取一个要发送的数据
(2)把这个数据构造成一个UDP的请求并发送
(3)读取服务器的 UDP 响应,并解析
(4)把解析好的结果显示出来
代码实现:
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//UDP版本的回显客户端
public class UdpEcCl {
private DatagramSocket socket = null;
private String serverIp = null;
private int serverPort = 0;
public UdpEcCl(String serverIp,int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true){
//1.从控制台读取一个要发送的数据
System.out.print(">");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("bye");
break;
}
//2.把这个数据构造成一个UDP的请求并发送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
//3.读取服务器的 UDP 响应,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
//4.把解析好的结果显示出来
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEcCl clinet = new UdpEcCl("127.0.0.1",9090);
clinet.start();
}
}
加注释版本:
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//UDP版本的回显客户端
public class UdpEcCl {
//1.首先需要创建出来一个socket对象
//网络编程需要操控网卡,就需要创建一个socket对象进行远程操控
private DatagramSocket socket = null;
//需要将ip和端口存一下
private String serverIp = null;
private int serverPort = 0;
//写一个构造方法
//指定服务器的ip和服务器的端口;一次通信需要有俩ip,俩端口
//客户端的ip是127.0.0.1;端口是系统自动分配的
//服务器的ip和端口也需要告诉客户端,才能顺利将消息发给服务器
public UdpEcCl(String serverIp,int serverPort) throws SocketException {
socket = new DatagramSocket();
this.serverIp = serverIp;
this.serverPort = serverPort;
}
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true){
//1.从控制台读取一个要发送的数据
System.out.print(">");
//客户端发送的请求
String request = scanner.next();
//对客户端输入的内容进行进行简单的翻译
if(request.equals("exit")){
System.out.println("bye");
break;
}
//2.把这个数据构造成一个UDP的请求并发送
//构造这个packet的时候,需要把serverIp和port都传入过来
// 但是由于此处的ip地址需要填写的是32位的整数形式
//上述的IP地址是一个字符串,需要使用 InetAddress.getByName()来进行转换
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
//3.读取服务器的 UDP 响应,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
//4.把解析好的结果显示出来
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEcCl clinet = new UdpEcCl("127.0.0.1",9090);
clinet.start();
}
}
理解服务器与客户端端口:
对于服务器来说,端口必须是确定好的,对于客户端来说,端口是可以系统分配的。
来食堂买饭,商家给顾客的小票就相当于是端口号(小票上的编号究竟是多少,也是想当于是随机的,控制不了。就想在客户端这里手动指定端口是可行的,但是并不推荐这样。有可能指定的这个号码会被别人用了。)
客户端如果显示指定端口,就有可能和客户端电脑上的其他程序的端口就冲突了,这一冲突就可能会导致程序没有办法正常通信了。
那么为什么服务器这边指定的端口不怕重复呢?
服务器是程序员自己手里的机器,上面运行什么都是程序员可控的,程序员就可以安排哪个程序使用哪个端口,客户端的机器是在用户的手里,上面运行的程序也是不同的,是不可控的。
2.4.6服务器和客户端执行顺序:
一定是服务器先运行。服务器是被动的一方(类比于我们要去餐馆吃饭,一定是饭店先开门,才能去吃饭。)
第一步,服务器执行到receive进行阻塞。
第二步,客户端执行,客户端读取用户输入的内容。
第三步,客户端发送请求。
第四步,客户端等待响应。服务器收到请求,进行返回。【可以认为这两部分相同的时间执行的】
第五步,服务器根据请求计算响应。process
第六步,服务器执行send,返回响应。
第七步,客户端receive,从阻塞中返回,读到响应
对于客户端服务器程序来说,一个客户端要给很多个服务器提供服务,也就需要构造出多个客户端来进行测试。