文章目录
- 前言
- 一、UDP与TCP
-
- [1.1 有连接与无连接](#1.1 有连接与无连接)
- [1.2 全双工](#1.2 全双工)
- [1.3 可靠传输与不可靠传输](#1.3 可靠传输与不可靠传输)
- [1.4 面向子节流与面向数据报](#1.4 面向子节流与面向数据报)
- 二、UDP回显服务器及客户端编写
- 三、UDP字典服务器
- 四、TCP回显服务器及客户端编写
- 五、数据序列化的方式
-
- [5.1 基于行文本的方式传输](#5.1 基于行文本的方式传输)
- [5.2 基于XML的格式](#5.2 基于XML的格式)
- [5.3 基于json](#5.3 基于json)
- [5.4 yml](#5.4 yml)
- [5.5 protobuffer(pb)](#5.5 protobuffer(pb))
前言
目前见到socket这个词,就可以认为是网络编程api的统称。操作系统提供的socket api不是一套而是有好几套。
(1)流式套接字:给TCP使用。
(2)数据包套接字:给UDP使用。
(3)Unix域套接字:不能跨主机通信,只是本地主机上的线程与线程之间的通信方式。
一、UDP与TCP
UDP和TCP都是传输层的协议,是给应用层提供服务的,但是两个协议之间的差异还是很大的。
1.1 有连接与无连接
UDP是无连接的,TCP是有连接的。
举例子来理解的话就是有连接相当于打电话,你必须等到对方接通你才能和他通话;无连接相当于短信,你想发就发,不用先接通。
对于UDP的无连接就是直接发送数据,TCP的有连接就是建立连接之后才能发送数据。
另外在计算机中的连接是一个抽象的概念,生活中我们谈到连接往往是把两个东西连起来,但是在计算机的领域,连接就是认为建立连接的双方各自保存对方的信息,此时就认为是建立了一个抽象的连接。
1.2 全双工
UDP和TCP都是全双工的。
至于全双工就是指一条通信链路能够双向通信,与之相对的就是半双工指的就是一条通信链路只能够单向通信。
对于全双工在代码中的体现就是后续使用socket既可以读也可以写。
1.3 可靠传输与不可靠传输
UDP是不可靠传输,TCP是可靠传输。
可靠指的不是一定百分百安全,只是说TCP会尽力去保证数据在传输的过程中少发生意外,尽可能多的去保留数据。而UDP就不一样了,它是不可靠的,只要把数据发送过去就行了,不管在这个过程中会发生什么。
通过上述不难发现,可靠与不可靠也决定了UDP与TCP的一部分特点。TCP要想实现可靠性必然会付出代价,因此它数据传输的速度是不如UDP的,但是UDP的可靠性也因此不如TCP。两者有着不同的适用场景,TCP适用于对数据要求高的,不能出错的场景,UDP适用于对数据的准确性要求不高,但是对传输速率要求高的场景。一般来说的话现在一些应用软件都是UDP和TCP混用。
1.4 面向子节流与面向数据报
文件操作是面向字节流的,TCP与其有着相同的特点也是面向字节流的,UDP则是面向数据报的。面向数据报就是指数据传输的单位是数据报,一次读写只能读写完整的数据报,不能搞半个也不搞一个半。
另外网络传输数据的基本单位涉及到几个术语:
(1)数据报Datagram UDP
(2)数据段Segment TCP
(3)数据包 Packet IP
(4)数据帧 Frame 数据链路层
虽然以上术语是有差别的,但是程序员对于以上术语都是混着用的不会做刻意区分,因此后续使用过程中也不必刻意区分。
二、UDP回显服务器及客户端编写
Echo称为"回显",正常的服务器你给它发送不同的请求就会返回不同的响应,此处回显的意思就是请求是什么就返回什么。实际上这就是最简单的客户端服务器程序,用来认识socket api的用法。
UdpEchoServer:
java
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
private DatagramSocket datagramSocket = null;
public UdpEchoServer(int port) throws SocketException {
datagramSocket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!!!");
while (true) {
// 1.读取请求并且解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
datagramSocket.receive(requestPacket);
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
// 2.处理计算请求信息
String response = this.process(request);
// 3.把响应返回客户端 客户端的ip以及端口号可以通过请求的数据包中获取
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 0, response.getBytes().length,
requestPacket.getSocketAddress());
datagramSocket.send(responsePacket);
// 打印日志 ip port 请求以及返回内容
System.out.printf("[%s:%d] req=%s reps=%s", requestPacket.getAddress(), requestPacket.getPort(), request, response);
System.out.println();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(4090);
udpEchoServer.start();
}
}
UdpEchoClient:
java
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket datagramSocket = null;
private String serverIp;
private int port;
public UdpEchoClient(String ip, int port) throws SocketException {
datagramSocket = new DatagramSocket();
this.serverIp = ip;
this.port = port;
}
public void start() throws IOException {
System.out.println("客户端启动!!!");
Scanner sc = new Scanner(System.in);
while (true) {
// 输入请求
System.out.println("请输入请求:");
String request = sc.nextLine();
// 打包请求并且发送请求
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), 0, request.getBytes().length, InetAddress.getByName(serverIp), port);
datagramSocket.send(requestPacket);
// 接收响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
datagramSocket.receive(responsePacket);
// 打印响应的内容
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("localhost", 4090);
client.start();
}
}
三、UDP字典服务器
对于字典服务器,和回显的区别在于你请求的是一个中文字符串,响应也就是要返回一个英语单词,因此我们需要在服务器端去存储对应的单词键值对即可,又因为我们前面实现了回显服务器,所以我们可以直接继承回显服务器的代码,然后添加单词键值对并且重写posses函数即可,至于客户端还以一样不需要去改变。
UdpDictServer:
java
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer {
private HashMap<String, String> dict = null;
public UdpDictServer(int port) throws SocketException {
super(port);
dict = new HashMap<>();
dict.put("hello", "你好");
dict.put("pig", "小猪");
dict.put("dog", "小狗");
dict.put("cat", "小猫");
}
@Override
public String process(String request) {
return (String) dict.getOrDefault(request, "未搜索到单词");
}
public static void main(String[] args) throws IOException {
UdpDictServer dictServer = new UdpDictServer(4090);
dictServer.start();
}
}
四、TCP回显服务器及客户端编写
TcpEchoServer:
java
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!!!");
while (true) {
// 建立连接
Socket clientSocket = serverSocket.accept();
// 建立线程池 这里建立的是可以自动扩容的线程池
ExecutorService pool=Executors.newCachedThreadPool();
// 为了方便多个客户端对服务器发起请求
// 这里使用主线程来处理这里的循环 然后使用多线程的放式去去处理每一个客户端的请求
// Thread t = new Thread(() -> {
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
//
// });
//
// t.start();
// 使用线程池的方式
pool.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
// 获取字节流对象
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
Scanner sc = new Scanner(inputStream);
while (true) {
// 两种情况
if (!sc.hasNext()) {
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
break;
}
// 获取请求
String request = sc.next();
// 处理请求
String response = process(request);
// 返回请求
outputStream.write(response.getBytes());
// 服务器打印日志
System.out.printf("[%s:%d] req=%s resp=%s", clientSocket.getInetAddress(), clientSocket.getPort(), request, response);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// 每次一个客户端请求的连接最后都要关闭 否则当多个客户端连接同一个服务器的时候就会出现文件描述符表爆满的问题
// 这个问题简单想一下就会理解
clientSocket.close();
}
}
private String process(String request) {
return request + '\n';
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(4090);
server.start();
}
}
TcpEchoClient:
java
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
Socket socket = null;
public TcpEchoClient(String ip, int port) throws IOException {
// 这里根据ip和port号自动和服务器建立连接
// 具体完成的操作都是系统内核完成的
socket = new Socket(ip,port);
}
private void start() {
System.out.println("客户端启动!!!");
Scanner sc = new Scanner(System.in);
// 获取字节流对象
try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) {
Scanner scNetwork = new Scanner(inputStream);
while (true) {
System.out.println("请输入要发送的内容:");
// 输入请求
String request = sc.next();
request += '\n';
// 发送请求
outputStream.write(request.getBytes());
// 两中情况
// 第一种:tcp连接断开 返回false
// 第二种:有请求返回
if (!scNetwork.hasNext()) {
break;
}
String response = scNetwork.next();
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("localhost", 4090);
client.start();
}
}
在tcp服务器代码的编写中我们发现服务器无法去应对多个客户端的请求,因此我们使用多线程或者线程池的方式,让每一个线程去处理一个请求。此时不免去联想到一个问题如果在一个场景当中,服务器收到的不同客户端的请求越来越多,我们难道需要不停的去创建线程吗,如果真是这样服务器肯定支撑不住。事实上对于这种情况可以使用IO多路复用+分布式的方法。
分布式我们都知道,那么IO多路复用指的是什么?其实就是使用一个线程去管理多个socket(可以将socket理解成客户端的请求),这些socket往往不是同时有数据需要处理,而是同一时刻只有少数的socket需要去读取数据。
五、数据序列化的方式
5.1 基于行文本的方式传输
这种格式是自定义的,只要确保客户端与服务器使用的是同一套规则即可。缺点就是不好用,可维护性差。
5.2 基于XML的格式
XML是通过成对的标签来进行组织的。
xml
<request>
<userId>1234</userId>
</request>
5.3 基于json
当前最流行最广泛的使用方式,是以键值对的形式,可读性非常好而且比XML简洁。
xml
{
userId: 1234
}
5.4 yml
和前两种是类似的,是基于缩进的格式,使用缩进来表达包含/嵌套关系。
xml
request
userId: 1234
position: "180E40N"
5.5 protobuffer(pb)
前面几种说到底还是文本格式,肉眼还能看懂,这里的pb就是二进制格式了,可以对数据进行进一步的整理和压缩,虽然可读性不好但是对空间进行最充分的利用,节省网络带宽,效率也最高,适用于对传输效率高的场景。