[{"data":1,"prerenderedAt":204},["ShallowReactive",2],{"content:\u002Fprojects\u002Fmydb\u002Fmydb10":3,"surround:\u002Fprojects\u002Fmydb\u002Fmydb10":193},{"id":4,"title":5,"body":6,"categories":170,"date":172,"description":173,"draft":174,"extension":175,"image":176,"meta":177,"navigation":179,"path":180,"permalink":176,"published":176,"readingTime":181,"recommend":176,"references":176,"seo":186,"sitemap":187,"stem":188,"tags":189,"type":191,"__hash__":192},"content\u002Fposts\u002Fprojects\u002Fmydb\u002Fmydb10.md","MYDB 10. 服务端客户端的实现及其通信规则",{"type":7,"value":8,"toc":163},"minimark",[9,33,37,40,44,47,50,61,64,72,75,81,84,90,93,99,103,106,109,112,118,121,124,130,133,139,142,148,151,157,160],[10,11,12,13,20,21,26,27,32],"p",{},"本章涉及代码都在 ",[14,15,19],"a",{"href":16,"rel":17},"https:\u002F\u002Fgithub.com\u002FCN-GuoZiyang\u002FMYDB\u002Ftree\u002Fmaster\u002Fsrc\u002Fmain\u002Fjava\u002Ftop\u002Fguoziyang\u002Fmydb\u002Fbackend\u002Fserver",[18],"nofollow","backend\u002Fserver"," 、 ",[14,22,25],{"href":23,"rel":24},"https:\u002F\u002Fgithub.com\u002FCN-GuoZiyang\u002FMYDB\u002Ftree\u002Fmaster\u002Fsrc\u002Fmain\u002Fjava\u002Ftop\u002Fguoziyang\u002Fmydb\u002Fclient",[18],"client"," 与 ",[14,28,31],{"href":29,"rel":30},"https:\u002F\u002Fgithub.com\u002FCN-GuoZiyang\u002FMYDB\u002Ftree\u002Fmaster\u002Fsrc\u002Fmain\u002Fjava\u002Ftop\u002Fguoziyang\u002Fmydb\u002Ftransport",[18],"transport"," 中。",[34,35,36],"h3",{"id":36},"前言",[10,38,39],{},"NYDB 被设计为 C\u002FS 结构，类似于 MySQL。支持启动一个服务器，并有多个客户端去连接，通过 socket 通信，执行 SQL 返回结果。",[34,41,43],{"id":42},"cs-通信","C\u002FS 通信",[10,45,46],{},"MYDB 使用了一种特殊的二进制格式，用于客户端和服务端通信。当然如果嫌麻烦的话，其实直接用明文也不是不可以。",[10,48,49],{},"传输的最基本结构，是 Package：",[51,52,58],"pre",{"className":53,"code":55,"language":56,"meta":57},[54],"language-java","public class Package {\n    byte[] data;\n    Exception err;\n}\n","java","",[59,60,55],"code",{"__ignoreMap":57},[10,62,63],{},"每个 Package 在发送前，由 Encoder 编码为字节数组，在对方收到后同样会由 Encoder 解码成 Package 对象。编码和解码的规则如下：",[51,65,70],{"className":66,"code":68,"language":69},[67],"language-text","[Flag][data]\n","text",[59,71,68],{"__ignoreMap":57},[10,73,74],{},"若 flag 为 0，表示发送的是数据，那么 data 即为这份数据本身；如果 flag 为 1，表示发送的是错误，data 是 Exception.getMessage() 的错误提示信息。如下：",[51,76,79],{"className":77,"code":78,"language":56,"meta":57},[54],"public class Encoder {\n    public byte[] encode(Package pkg) {\n        if(pkg.getErr() != null) {\n            Exception err = pkg.getErr();\n            String msg = \"Intern server error!\";\n            if(err.getMessage() != null) {\n                msg = err.getMessage();\n            }\n            return Bytes.concat(new byte[]{1}, msg.getBytes());\n        } else {\n            return Bytes.concat(new byte[]{0}, pkg.getData());\n        }\n    }\n\n    public Package decode(byte[] data) throws Exception {\n        if(data.length \u003C 1) {\n            throw Error.InvalidPkgDataException;\n        }\n        if(data[0] == 0) {\n            return new Package(Arrays.copyOfRange(data, 1, data.length), null);\n        } else if(data[0] == 1) {\n            return new Package(null, new RuntimeException(new String(Arrays.copyOfRange(data, 1, data.length))));\n        } else {\n            throw Error.InvalidPkgDataException;\n        }\n    }\n}\n",[59,80,78],{"__ignoreMap":57},[10,82,83],{},"编码之后的信息会通过 Transporter 类，写入输出流发送出去。为了避免特殊字符造成问题，这里会将数据转成十六进制字符串（Hex String），并为信息末尾加上换行符。这样在发送和接收数据时，就可以很简单地使用 BufferedReader 和 Writer 来直接按行读写了。",[51,85,88],{"className":86,"code":87,"language":56,"meta":57},[54],"public class Transporter {\n    private Socket socket;\n    private BufferedReader reader;\n    private BufferedWriter writer;\n\n    public Transporter(Socket socket) throws IOException {\n        this.socket = socket;\n        this.reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));\n        this.writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));\n    }\n\n    public void send(byte[] data) throws Exception {\n        String raw = hexEncode(data);\n        writer.write(raw);\n        writer.flush();\n    }\n\n    public byte[] receive() throws Exception {\n        String line = reader.readLine();\n        if(line == null) {\n            close();\n        }\n        return hexDecode(line);\n    }\n\n    public void close() throws IOException {\n        writer.close();\n        reader.close();\n        socket.close();\n    }\n\n    private String hexEncode(byte[] buf) {\n        return Hex.encodeHexString(buf, true)+\"n\";\n    }\n\n    private byte[] hexDecode(String buf) throws DecoderException {\n        return Hex.decodeHex(buf);\n    }\n}\n",[59,89,87],{"__ignoreMap":57},[10,91,92],{},"Packager 则是 Encoder 和 Transporter 的结合体，直接对外提供 send 和 receive 方法：",[51,94,97],{"className":95,"code":96,"language":56,"meta":57},[54],"public class Packager {\n    private Transporter transpoter;\n    private Encoder encoder;\n\n    public Packager(Transporter transpoter, Encoder encoder) {\n        this.transpoter = transpoter;\n        this.encoder = encoder;\n    }\n\n    public void send(Package pkg) throws Exception {\n        byte[] data = encoder.encode(pkg);\n        transpoter.send(data);\n    }\n\n    public Package receive() throws Exception {\n        byte[] data = transpoter.receive();\n        return encoder.decode(data);\n    }\n\n    public void close() throws Exception {\n        transpoter.close();\n    }\n}\n",[59,98,96],{"__ignoreMap":57},[34,100,102],{"id":101},"server-和-client-的实现","Server 和 Client 的实现",[10,104,105],{},"Server 和 Client，偷懒直接使用了 Java 的 socket。",[10,107,108],{},"Server 启动一个 ServerSocket 监听端口，当有请求到来时直接把请求丢给一个新线程处理。这部分应该直接背板了。",[10,110,111],{},"HandleSocket 类实现了 Runnable 接口，在建立连接后初始化 Packager，随后就循环接收来自客户端的数据并处理：",[51,113,116],{"className":114,"code":115,"language":56,"meta":57},[54],"Packager packager = null;\ntry {\n    Transporter t = new Transporter(socket);\n    Encoder e = new Encoder();\n    packager = new Packager(t, e);\n} catch(IOException e) {\n    e.printStackTrace();\n    try {\n        socket.close();\n    } catch (IOException e1) {\n        e1.printStackTrace();\n    }\n    return;\n}\nExecutor exe = new Executor(tbm);\nwhile(true) {\n    Package pkg = null;\n    try {\n        pkg = packager.receive();\n    } catch(Exception e) {\n        break;\n    }\n    byte[] sql = pkg.getData();\n    byte[] result = null;\n    Exception e = null;\n    try {\n        result = exe.execute(sql);\n    } catch (Exception e1) {\n        e = e1;\n        e.printStackTrace();\n    }\n    pkg = new Package(result, e);\n    try {\n        packager.send(pkg);\n    } catch (Exception e1) {\n        e1.printStackTrace();\n        break;\n    }\n}\n",[59,117,115],{"__ignoreMap":57},[10,119,120],{},"处理的核心是 Executor 类，Executor 调用 Parser 获取到对应语句的结构化信息对象，并根据对象的类型，调用 TBM 的不同方法进行处理。具体不再赘述。",[10,122,123],{},"top.guoziyang.mydb.backend.Launcher 类，则是服务器的启动入口。这个类解析了命令行参数。很重要的参数就是 -open 或者 -create。Launcher 根据两个参数，来决定是创建数据库文件，还是启动一个已有的数据库。",[51,125,128],{"className":126,"code":127,"language":56,"meta":57},[54],"private static void createDB(String path) {\n    TransactionManager tm = TransactionManager.create(path);\n    DataManager dm = DataManager.create(path, DEFALUT_MEM, tm);\n    VersionManager vm = new VersionManagerImpl(tm, dm);\n    TableManager.create(path, vm, dm);\n    tm.close();\n    dm.close();\n}\n\nprivate static void openDB(String path, long mem) {\n    TransactionManager tm = TransactionManager.open(path);\n    DataManager dm = DataManager.open(path, mem, tm);\n    VersionManager vm = new VersionManagerImpl(tm, dm);\n    TableManager tbm = TableManager.open(path, vm, dm);\n    new Server(port, tbm).start();\n}\n",[59,129,127],{"__ignoreMap":57},[10,131,132],{},"客户端连接服务器的过程，也是背板。客户端有一个简单的 Shell，实际上只是读入用户的输入，并调用 Client.execute()。",[51,134,137],{"className":135,"code":136,"language":56,"meta":57},[54],"public byte[] execute(byte[] stat) throws Exception {\n    Package pkg = new Package(stat, null);\n    Package resPkg = rt.roundTrip(pkg);\n    if(resPkg.getErr() != null) {\n        throw resPkg.getErr();\n    }\n    return resPkg.getData();\n}\n",[59,138,136],{"__ignoreMap":57},[10,140,141],{},"RoundTripper 类实际上实现了单次收发动作：",[51,143,146],{"className":144,"code":145,"language":56,"meta":57},[54],"public Package roundTrip(Package pkg) throws Exception {\n    packager.send(pkg);\n    return packager.receive();\n}\n",[59,147,145],{"__ignoreMap":57},[10,149,150],{},"最后附上客户端的启动入口，很简单，把 Shell run 起来即可：",[51,152,155],{"className":153,"code":154,"language":56,"meta":57},[54],"public class Launcher {\n    public static void main(String[] args) throws UnknownHostException, IOException {\n        Socket socket = new Socket(\"127.0.0.1\", 9999);\n        Encoder e = new Encoder();\n        Transporter t = new Transporter(socket);\n        Packager packager = new Packager(t, e);\n\n        Client client = new Client(packager);\n        Shell shell = new Shell(client);\n        shell.run();\n    }\n}\n",[59,156,154],{"__ignoreMap":57},[10,158,159],{},"今天是 2021 年 12 月 26 日，圣诞节。",[10,161,162],{},"战无不胜的毛泽东思想万岁！",{"title":57,"searchDepth":164,"depth":164,"links":165},4,[166,168,169],{"id":36,"depth":167,"text":36},3,{"id":42,"depth":167,"text":43},{"id":101,"depth":167,"text":102},[171],"项目","2021-12-25 18:26:00","MYDB 采用 C\u002FS 结构，类似于 MySQL，允许多个客户端通过 socket 连接到服务器，执行 SQL 查询并返回结果。通信采用了一种特殊的二进制格式，尽管也可以选择明文传输，以简化实现。服务端与客户端之间的基本传输结构确保了数据的有效交流和处理。",false,"md",null,{"slots":178},{},true,"\u002Fprojects\u002Fmydb\u002Fmydb10",{"text":182,"minutes":183,"time":184,"words":185},"6 min read",5.685,341100,1137,{"title":5,"description":173},{"loc":180},"posts\u002Fprojects\u002Fmydb\u002Fmydb10",[56,190],"mydb","tech","QvMxvc5lv2zuZYiWxUGnshIZEUCR20gaiAq11QJkIo0",[194,199],{"title":195,"path":196,"stem":197,"date":198,"type":191,"children":-1},"MYDB 9. 字段与表管理","\u002Fprojects\u002Fmydb\u002Fmydb9","posts\u002Fprojects\u002Fmydb\u002Fmydb9","2021-12-25 15:44:00",{"title":200,"path":201,"stem":202,"date":203,"type":191,"children":-1},"CSAPP LAB 环境搭建","\u002Ffiddling\u002Fcsapplab0","posts\u002Ffiddling\u002Fcsapplab0","2021-12-27 00:09:00",1787554445538]