但使龙城飞将在,只缘身在此山中
之前在 BIO、NIO 入门(Netty 先导) 一文中聊了 socket ,本文想把视野拉大,从计算机网络的纬度来聊聊 socket
上图对比给出了 ISO(International Standardization Organization 国际标准化组织)的 OSI (Open System Interconnect Reference Model 开放式系统互联参考模型)七层网络参考模型,这个模型是标准,是参考,而现在使用最广的实现是 TCP/IP 的四层模型。
Tips:这里提一下,网上有说 TCP/IP 四层模型的,也有说五层的,区别在于 四层是把 数据链路层 和 物理层 合并为一个 网络接口层,这两种说法都不为错。
聊点真实的
感觉上图说的还是有点虚,懂了但是好像没完全懂。那这里我们聊点真实的,说说我们真正在用的 TCP/IP 网络模型,以 五层分层为例,这样更清楚一些。
总的来说,我们日常编程玩的大部分都在 应用层 和 传输层 这两块。OK 这里提到 Socket 编程,那么 Socket 在哪?用来干嘛?继续来点真实的
所谓 Socket 编程,其实就是在串联 应用层 和 传输层以达到我们的需求目的。主要就是在操作 传输层的协议,而我们主流的传输层协议就是 TCP 和 UDP 所以重点关注这两个协议即可。
不得不说的 TCP 和 UDP
时常被问到的一个问题:TCP 和 UDP 什么区别? 通常答案是这样的
TCP 是面向连接的;UDP 面向无连接。
TCP 是可靠的,无差错、不丢失、保证顺序;UDP 不可靠,不保证
TCP 面向字节流,发送时是一个流;UDP 面向数据报 ,一个一个发送
TCP 可以提供 流量控制,拥塞控制;UDP 没有
以上大概是常用的回答了,但是对于自己理解来说,似乎总有些说不清道不明的迷糊在里面。比如
连接到底是什么?真的是在网线中维系了这样一种抽象的链路吗?
不是的,”连接“ 意思上感觉是通路,其实其只存在于两端,即需要通信的两端。
建立连接其实就是在通信两端建立一种数据结构,然后维持这种数据结构,以保证通信双方可以互相识别对方发送过来的信息,并用这样的数据结构保证面向连接的特性。
来点真实的:连接在两端,而非通路。可以理解为两端数据结构的协调,这个数据结构类比 Java 可以理解为 Class
两边数据结构状态一致,符合 TCP 的规则,那就认为连接存在,如果对不上就是连接断了。
而所谓的可靠、顺序、流传输,这些也都是通过两端的数据结构来保证。可靠是数据结构在点名,顺序是数据结构进行了排序、流传输是数据结构对单个包进行了归并统一发送。
生活的例子:
我觉得很形象的就是我们买东西时 收发快递 ,你与商家就是两端,网络层就相当于你们彼此知道地址,而发送包裹交给物流,具体发生什么事你们根本不知道,你们只关心结果,而 ”数据结构“ 就是商家和买家各自对于包裹异常所作出的行为补正,用于保证快递正确到达且完好无损。
经过上面的铺垫,可以知道,Socket 编程其实就是端对端的编程,其控制的是端上的逻辑,有时候我们又会听到一起其他的相关名词。如: Socket 文件、Socket 句柄。 这个其实是在我们最常用的 Linux 服务器上,Socket 就是以一种文件的形式存在的,所以收到内存的大小的限制,Socket 连接数同样也是有限制的,不然迟早要冲垮你的服务器。
下图是 Socket 针对 TCP 协议 Java 版的伪代码。其他语言逻辑类似。
这里 服务端 绑定好端口,调用 accept 方法,等待一个连接请求,完成 TCP 的三次握手,成功后会返回这个链接的 Socket 对象,那么如果需要处理多个连接就需要多次调用 accept 方法,所以经常可以看到 accept 方法是套在循环里的。
可以发现客户端在创建 Socket 连接的时候似乎不需要绑定端口,这是因为系统会为其随机分配一个端口进行连接,因为客户端只有我们自己在用,所以并不关心客户端端口是多少。
ok ,基于理论,这里我用 Java 简单实现了一个 RPC 接口,给大家提供一个实际的参考。
服务端统一接口
public interface RpcServer {
void stop();
void start() throws IOException;
void register(Class<?> serviceInterface, Class<?> impl);
boolean isRunning();
int getPort();
}
服务端默认实现
public class DefaultServer implements RpcServer {
private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private static final HashMap<String, Class> serviceRegistry = new HashMap<String, Class>();
private static boolean isRunning = false;
private int port;
public DefaultServer(int port) {
this.port = port;
}
@Override
public void stop() {
isRunning = false;
executor.shutdown();
}
@Override
public void start() throws IOException {
System.out.println("start server");
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(port));
System.out.println("====已注册服务====");
for (Map.Entry<String, Class> entry : serviceRegistry.entrySet()) {
System.out.println("注册服务名:"+entry.getKey()+" 实现:"+entry.getValue());
}
System.out.println("==========");
try {
while (true) {
executor.execute(new ServerTask(serverSocket.accept()));
}
} finally {
serverSocket.close();
}
}
@Override
public void register(Class<?> serviceInterface, Class<?> impl) {
serviceRegistry.put(serviceInterface.getName(), impl);
}
@Override
public boolean isRunning() {
return isRunning;
}
@Override
public int getPort() {
return port;
}
private class ServerTask implements Runnable {
private Socket socket;
public ServerTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
ObjectInputStream inputStream = null;
ObjectOutputStream outputStream = null;
try {
inputStream = new ObjectInputStream(socket.getInputStream());
String serviceName = inputStream.readUTF();
String methodName = inputStream.readUTF();
Class<?>[] parameterTypes = (Class<?>[]) inputStream.readObject();
Object[] arguments = (Object[]) inputStream.readObject();
Class<?> serviceClass = serviceRegistry.get(serviceName);
if (serviceClass == null) {
throw new ClassNotFoundException(serviceName + " not found");
}
Method method = serviceClass.getMethod(methodName, parameterTypes);
Object result = method.invoke(serviceClass.newInstance(), arguments);
outputStream = new ObjectOutputStream(socket.getOutputStream());
outputStream.writeObject(result);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
客户端实现
public class RPCClient<T> {
public static <T> T getRemoteProxyObj(final Class<?> serviceInterface, final InetSocketAddress addr) {
return (T) Proxy.newProxyInstance(serviceInterface.getClassLoader(), new Class[]{serviceInterface},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket socket = null;
ObjectOutputStream output = null;
ObjectInputStream input = null;
try {
socket = new Socket();
socket.connect(addr);
output = new ObjectOutputStream(socket.getOutputStream());
output.writeUTF(serviceInterface.getName());
output.writeUTF(method.getName());
output.writeObject(method.getParameterTypes());
output.writeObject(args);
// 4.同步阻塞等待服务器返回应答,获取应答后返回
input = new ObjectInputStream(socket.getInputStream());
return input.readObject();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (socket != null) socket.close();
if (output != null) output.close();
if (input != null) input.close();
}
return null;
}
});
}
}
demo 类
public interface Demo {
String getName(String name);
}
public class DemoAImpl implements Demo{
@Override
public String getName(String name) {
return "AAAAA "+name;
}
}
提供服务
public class ProviderTest {
public static void main(String[] args) throws IOException {
RpcServer rpcServer = new DefaultServer(8088);
rpcServer.register(Demo.class, DemoAImpl.class);
rpcServer.start();
}
}
客户端调用
public class RpcTest {
public static void main(String[] args) {
Demo demo = RPCClient.getRemoteProxyObj(Demo.class, new InetSocketAddress("localhost", 8088));
System.out.println(demo.getName("nice"));
}
}
说明一下
这种编程方式其实是非常原始的,有极大的改进空间,比如用 NIO 或者 Netty 都可以进行优化 BIO、NIO 入门(Netty 先导),还有数据传输的方式,篇幅原因,这里埋个坑,我们下期继续聊。
千回百转,这里是 dying 搁浅。