目录
Socket套接字
概念
Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
分类
Socket套接字主要针对传输层协议划分为如下三类:
- 流套接字:使用传输层TCP协议
TCP,即Transmission Control Protocol(传输控制协议),传输层协议。
以下为TCP的特点:
- 有连接
- 可靠传输
- 面向字节流
- 有接收缓冲区,也有发送缓冲区
- 大小不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
- 数据报套接字:使用传输层UDP协议
UDP,即User Datagram Protocol(用户数据报协议),传输层协议。
以下为UDP的特点:
- 无连接
- 不可靠传输
- 面向数据报
- 有接收缓冲区,无发送缓冲区
- 大小受限:一次最多传输64k
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每次接收1个字节。
- 原始套接字:原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。简单了解即可。
UDP VS TCP
具体的网络编程
写一个应用程序,让这个程序可以网络通信,这里就需要调用传输层的api
传输层提供协议主要是两个:
- UDP;
- TCP。
两套不同的api => socket api
UDP和TCP之间的主要特点对比:
- UDP:无连接,不可靠传输,面向数据报,全双工
- TCP:有连接,可靠传输,面向字节流,全双工
有连接 VS 无连接
- TCP进行编程的时候,也是存在类似的建立连接的过程(打电话:打过去对方接通才能说话)
- UDP:发微信/短信就是不需要建立连接就能进行通信的
- 这里所谓的“连接”是抽象的。客户端和服务器之间,使用内存保存对端的信息,双方都保存这个信息,此时“连接”就出现了。这里最大的区别是一个客户端可以连接多个服务器,一个服务器也可以对应多个客户端的连接(多对多)
可靠传输 VS 不可靠传输
- 可靠传输,不是说A给B发的消息100%能到(这个要求太难了),A尽可能把消息传给B,并且传输失败的时候,A能感知到;或者传输成功的时候,A也能感知到
- TCP可靠(传输效率更低),UDP不可靠(传输效率更高),但TCP比UDP更安全这句话是错误的
注意:谈到“网络安全”
指的是如果你传输的数据是否容易被黑客截获以及如果被截获后是否会泄露一些重要信息。安全和入侵,破解,加密反编译。
面向字节流 VS 面向数据报
- TCP和文件操作类似,都是“流”式的(由于这里传输的单位是字节,称为字节流)
接100ml水,可以一次接100ml,也可以一次接50ml分两次,也可以一次接10ml分十次
通过tcp读写100字节数据,可以一次读写100字节,也可以一次读写50字节分两次,也可以一次读写10字节分十次
- UDP是面向数据报,读写的基本单位,是一个UDP数据报(包含了一系列数据/属性)
全双工 VS 半双工
- 全双工:一个通道可以双向通信
- 半双工:一个通道只能单向通信
UDP数据报套接字编程
两个核心的类:DatagramSocket
、DatagramPacket
DatagramSocket API
DatagramSocket是一个socket对象,操作系统,使用文件这样的概念,来管理一些软硬件资源,网卡,操作系统也是使用文件的方式管理网卡的,表示网卡的这类文件,称为Socket文件。Java中的socket对象,就对应这系统里的socket文件(最终还是要落到网卡),要进行网络通信,必须得先有socket对象
DatagramSocket
是UDP Socket,用于发送和接收UDP数据报。
DatagramSocket
构造方法:
方法签名 | 方法说明 |
---|---|
DatagramSocket() | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端) |
DatagramSocket(int port) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
第一个构造方法用于客户端,使用哪个端口由系统自动分配
第二个构造方法用于服务端,使用哪个端口手动指定
关于“五元组”:源IP、源端口、目的IP、目的端口、协议类型
其中的源端口和目的端口,此时就需要给客户端、服务器各自分配端口号
客户端给服务器发数据:源端口就是客户端的端口,目的端口就是服务器的端口
服务器给客户端发数据:源端口就是服务器的端口,目的端口就是客户端的端口
IP地址:对于服务器来说,需要有一个固定的端口号,方便其他客户端找到;对于客户端来说,端口号不能指定固定值
一个客户端的主机,上面运行的程序很多,天知道你手动指定的端口是不是被别的程序占用了,让系统自动分配一个端口是更明智的选择。
服务器是完全在程序员手里控制的,程序员可以把服务器上的多个程序安排好,让他们使用不同的端口
DatagramSocket
方法:
方法签名 | 方法说明 |
---|---|
void receive(DatagramPacket p) | 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) | 从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() | 关闭此数据报套接字 |
注意:receive
这里传入的是个空对象,这是服务器先创建好放入的,然后一直阻塞等待着的,然后请求来了后再把数据解析放进去的
DatagramPacket API
DatagramPacket
是UDP Socket发送和接收的数据报。
DatagramPacket
构造方法:
方法签名 | 方法说明 |
---|---|
DatagramPacket(byte[] buf, int length) | 构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度作为存储数据的空间(第二个参数length) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造一个DatagramPacket以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号 |
DatagramPacket
这个对象里就包含着通信双方的IP和Port
DatagramPacket
方法:
方法签名 | 方法说明 |
---|---|
InetAddress getAddress() | 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号 |
byte[] getData() | 获取数据报中的数据(UDP数据报的载荷部分,即完整的应用层数据报) |
构造UDP发送的数据报时,需要传入 SocketAddress
,该对象可以使用 InetSocketAddress
来创建。
InetSocketAddress API
InetSocketAddress
( SocketAddress
的子类 )构造方法:
方法签名 | 方法说明 |
---|---|
InetSocketAddress(InetAddress addr, int port) | 创建一个Socket地址,包含IP地址和端口号 |
一个服务器要给很多客户端提供服务,也不知道客户端什么时候来,只能“时刻准备着”,随时准备提供服务
服务器步骤:
- 读取数据,并解析(固定套路)
- 根据请求,计算出响应(对于回显服务器不关心这个过程,请求是啥就返回啥响应。但商业级的服务器主要的代码都是在这一步)
- 把响应写回客户端(固定套路)
客户端步骤:
- 从控制台读取用户输入内容
- 构造请求对象,并发给服务器
- 读取服务器的响应,并解析出响应内容
- 显示到屏幕上
UDP回显客户端服务器
服务器
//UDP回显服务器
//客户端法的请求是啥,服务器返回的相应就是啥
public class UdpEchoServer {
private DatagramSocket socket = null;//socket对象
//参数是服务器要绑定的端口(相当于我这个店开在哪里)
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);//这里如果new失败了,最典型的情况是,端口号被占用了
}
//启动服务器
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. 把相应写回给客户端
//此时需要告诉网卡,要发的内容是啥,要发给谁
//前两个参数是关于要发的内容是啥以及长度
//最后一个参数是要发给谁
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//记录日志
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(9090);//这个端口号是随便写的,服务器启动要固定绑定到9090
server.start();
}
}
//运行结果1
服务器启动
[/127.0.0.1:55735]req:你好,resp:你好//55735就是系统随机分配的端口号,req就是请求,resp就是响应
[/127.0.0.1:55735]req:hello,resp:hello
//运行结果2
服务器启动
[/127.0.0.1:64432]req:你好1,resp:你好1//64432这是第一个客户端的端口号
[/127.0.0.1:64433]req:你好2,resp:你好2//64433这是第二个客户端的端口号
客户端
//UDP的回显客户端
public class UdpEchoClient {
private DatagramSocket socket=null;
private String serverIp;//形如 127.0.0.1 字符串格式的IP
private int serverPort;
//参数是服务器的ip和服务器的端口
public UdpEchoClient(String ip,int port) throws SocketException {
serverIp=ip;
serverPort=port;
//这个new操作就不再指定端口了,让系统自动分配一个空闲窗口
socket=new DatagramSocket();
}
//启动客户端,让客户端反复从控制台读取用户输入的内容,把这个内容构造成UDP请求,发给服务器,再读取服务器返回的UDP响应
//最终显示在客户端屏幕上
public void start() throws IOException {
Scanner sc=new Scanner(System.in);
System.out.println("客户端启动");
while(true){
//1.从控制台读取用户输入内容
System.out.print("->");//命令提示符,提示用户输入字符串
String request=sc.next();
//2.构造请求对象,并发给服务器
//前两个参数是关于要发的内容是啥以及长度
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);//发给哪个ip,哪个端口
//上面需要的是InetAddress对象,通过InetAddress的静态方法getByName来将serverIp构造成InetAddress对象
socket.send(requestPacket);
//3.读取服务器的响应,并解析出响应内容
DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
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 {
UdpEchoClient client=new UdpEchoClient("127.0.0.1",9090);//服务器Ip是前者,端口号和服务器的是一样的
//客户端接下来要访问9090这个端口
client.start();
}
}
//运行结果1
客户端启动
->你好//我客户端这边输入的请求
你好//服务器返回的响应
->hello//我客户端这边输入的请求
hello//服务器返回的响应
//运行结果2
客户端启动//这是第一个客户端
->你好1
你好1
客户端启动//这是第二个客户端
->你好2
你好2
注意:要先启动服务器再启动客户端
IDEA上启动多个程序要稍微设置一下
1.
2.
不这么设置就无法同时启动上面的两个客户端的情况
服务器和客户端的工作流程
后续任意服务器客户端的工作流程都是如此,只是计算响应中的process
方法更加复杂
网络环境的现状,NAT机制是主流,在NAT机制下,把IP地址分为了外网IP和内网IP(内网IP不能直接访问),而“我的电脑”没有外网IP,获取外网IP的方法则是找到一个“有外网IP的电脑”即云服务器。
UDP翻译客户端服务器
注意:服务器部分继承了上面回显服务器的start()
,客户端是一样的
//翻译服务器,继承了回显服务器,但要将process重写
public class UdpDictServer extends UdpEchoServer{//继承了UdpEchoServer回显服务器
private Map<String,String> dict=new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("fuck","我超");
//可以在这里继续添加千千万万个单词,使每个单词都有对应的翻译
}
public static void main(String[] args) throws IOException {
UdpDictServer server=new UdpDictServer(9090);
//start不需要再写一遍了,直接复用UdpEchoServer的start
server.start();
}
//是复用了之前的代码,但又要作出调整
@Override
public String process(String request){
//把请求对应单词的翻译,给返回回去
return dict.getOrDefault(request,"该词没有查询到");
}
}
//运行结果
//服务器部分
服务器启动
[/127.0.0.1:53183]req:dog,resp:小狗
[/127.0.0.1:53183]req:cat,resp:小猫
[/127.0.0.1:53183]req:fuck,resp:我超
[/127.0.0.1:53183]req:aaa,resp:该词没有查询到
//客户端部分
客户端启动
->dog
小狗
->cat
小猫
->fuck
我超
->aaa
该词没有查询到