两个人的孤独

  两个人的孤独,大抵是,你每发出去一句话,都要经由无数网络、由几百个计算机处理后,出在他的面前,而他就在你不远处。

  连接建立之后

  Openfire使用MINA网络框架,并设置ConnectionHandler为MINA的处理器,连接的启停、消息的收发,都在这个类中做中转。这是我们上一章《连接管理》中分析的内容。

  那么,当客户端与服务器的建立起连接以后,信息交换中,消息到了ConnectionHandler之后,是如何实现路由的,本文来一探究竟。

  ConnectionHandler类,MINA的处理器

  ConnectionHandler是个抽象类,openfire中的有四种handle,分别为:ServerConnectionHandler、ClientConnectionHandler、ComponentConnectionHandler、MultiplexerConnectionHandler,代表了S2S、C2S等四种不同的消息类型,这四个handle都继承自ConnectionHandler。

  而ConnectionHandler继承org.apache.mina.core.service.IoHandlerAdapter,IoHandlerAdapter实现了IoHandler接口。

  类关系如下:

  1. |-- IoHandler(接口)
  2. |-- IoHandlerAdapter(默认的空实现,实际的handler继承它就行)
  3. |-- ConnectionHandler
  4. |-- ServerConnectionHandler
  5. |-- ClientConnectionHandler
  6. |-- ComponentConnectionHandler
  7. |-- MultiplexerConnectionHandler

  IoHandler中定义了消息响应中所需要的一系列方法:

  1. public interface IoHandler
  2. {
  3. //创建session
  4. public void sessionCreated(IoSession session) throws Exception
  5. //开启session
  6. public void sessionOpened(IoSession iosession) throws Exception;
  7. //关闭session
  8. public void sessionClosed(IoSession iosession) throws Exception;
  9. //session空闲
  10. public void sessionIdle(IoSession iosession, IdleStatus idlestatus) throws Exception;
  11. //异常处理
  12. public void exceptionCaught(IoSession iosession, Throwable throwable) throws Exception;
  13. //接收消息
  14. public void messageReceived(IoSession iosession, Object obj) throws Exception;
  15. //发送消息
  16. public void messageSent(IoSession iosession, Object obj) throws Exception;
  17. }

  ConnectionHandler中覆写这些方法,并注入到MINA的适配器NioSocketAcceptor中,当接收到连接与进行交互时,将相应调用ConnectionHandler中覆写的方法。

  消息路由

  下面分析ConnectionHandler的消息响应机制,以C2S的message消息为例。

  ConnectionHandler除了实现IoHandler内定义的方法外,还定义了如下三个抽象方法: 

  1. // 创建NIOConnection
  2. abstract NIOConnection createNIOConnection(IoSession session);
  3. // 创建StanzaHandler
  4. abstract StanzaHandler createStanzaHandler(NIOConnection connection);
  5. // 从数据库中获取闲置timeout
  6. abstract int getMaxIdleTime();

  这三个方法,在具体的Handler子类里面实现,在sessionOpened()中调用,根据连接类型创建不同的NIOConnection、StanzaHandler的对象。

   ConnectionHandler.sessionOpened()

  1. @Override
  2. public void sessionOpened(IoSession session) throws Exception {
  3.  
  4. final XMLLightweightParser parser = new XMLLightweightParser(StandardCharsets.UTF_8);
  5. session.setAttribute(XML_PARSER, parser);
  6.  
  7. final NIOConnection connection = createNIOConnection(session);
  8. session.setAttribute(CONNECTION, connection);
  9. session.setAttribute(HANDLER, createStanzaHandler(connection));
  10.  
  11. final int idleTime = getMaxIdleTime() / 2;
  12. if (idleTime > 0) {
  13. session.getConfig().setIdleTime(IdleStatus.READER_IDLE, idleTime);
  14. }
  15. }

  其中,NIOConnection是对IoSession的一次包装,将MINA框架的IoSession转化为Openfire的Connection。StanzaHandler负责数据节的处理。 

  当服务器接收到客户端发送的消息时,MINA框架调用IoHandler.messageReceived将消息传递到指定的处理器ConnectionHandler中的messageReceived()方法。

    ConnectionHandler.messageReceived()

  1. @Override
  2. public void messageReceived(IoSession session, Object message) throws Exception {
  3. StanzaHandler handler = (StanzaHandler) session.getAttribute(HANDLER);
  4. final XMPPPacketReader parser = PARSER_CACHE.get();
  5. updateReadBytesCounter(session);
  6. try {
  7. handler.process((String) message, parser);
  8. } catch (Exception e) {
  9. final Connection connection = (Connection) session.getAttribute(CONNECTION);
  10. if (connection != null) {
  11. connection.close();
  12. }
  13. }
  14. }

  消息由StanzaHandler处理,C2S消息具体的实现类是ClientStanzaHandler。

  StanzaHandler.process()方法如下:

  1. public void process(String stanza, XMPPPacketReader reader) throws Exception {
  2. boolean initialStream = stanza.startsWith("<stream:stream") || stanza.startsWith("<flash:stream");
  3. if (!sessionCreated || initialStream) {
  4. if (!initialStream) {
  5. ......
  6. }
  7. // Found an stream:stream tag...
  8. if (!sessionCreated) {
  9. sessionCreated = true;
  10. MXParser parser = reader.getXPPParser();
  11. parser.setInput(new StringReader(stanza));
  12. createSession(parser);
  13. }
  14. .....
  15. }
  16.  
  17. ......
  18. process(doc);
  19. }
  20. }  

  上面省略掉部分代码,可以看到执行了如下操作:

  (1)若Session未创建,则创建之

  (2)调用本类的process(Element doc)

  Session的创建中,涉及到Session的管理,路由表的构建等重点内容,在下一章再专门做讲解。这里先提两点:1、此处的Session,只是预创建,还未能用于通信;2、在与客户端完成资源绑定的时候,该Session才真正可用。

  而process(Element doc)如下,只保留了和message相关的代码: 

  1. private void process(Element doc) throws UnauthorizedException {
  2. if (doc == null) {
  3. return;
  4. }
  5.  
  6. // Ensure that connection was secured if TLS was required
  7. if (connection.getTlsPolicy() == Connection.TLSPolicy.required &&
  8. !connection.isSecure()) {
  9. closeNeverSecuredConnection();
  10. return;
  11. }
  12.  
  13. String tag = doc.getName();
  14. if ("message".equals(tag)) {
  15. Message packet;
  16. try {
  17. packet = new Message(doc, !validateJIDs());
  18. }
  19. catch (IllegalArgumentException e) {
  20. Log.debug("Rejecting packet. JID malformed", e);
  21. // The original packet contains a malformed JID so answer with an error.
  22. Message reply = new Message();
  23. reply.setID(doc.attributeValue("id"));
  24. reply.setTo(session.getAddress());
  25. reply.getElement().addAttribute("from", doc.attributeValue("to"));
  26. reply.setError(PacketError.Condition.jid_malformed);
  27. session.process(reply);
  28. return;
  29. }
  30. processMessage(packet);
  31. }
  32. ......
  33. }

  将Element转化为Message对象,然后在StanzaHandler.processMessage()中,调用包路由PacketRouterImpl模块发送消息。

  1. protected void processMessage(Message packet) throws UnauthorizedException {
  2. router.route(packet);
  3. session.incrementClientPacketCount();
  4. }

  Openfire有三种数据包:IQ、Message、Presence,对应的路由器也有三种:IQRouter、MessageRouter、PresenceRouter。

  PacketRouterImpl是对这三种路由器统一做包装,对于message消息,调用的是MessageRouter中的route()方法。

  PacketRouterImpl.route()如下:

  1. @Override
  2. public void route(Message packet) {
  3. messageRouter.route(packet);
  4. }

  MessageRouter.route()中消息的发送,分如下两步:

  (1)调用路由表,将消息发给Message中指定的接收者ToJID。

  (2)通过session,将消息原路返回给发送方(当发送方收到推送回来的消息,表示消息已发送成功)

  MessageRouter.route()代码如下:

  1. public void route(Message packet) {
  2. if (packet == null) {
  3. throw new NullPointerException();
  4. }
  5. ClientSession session = sessionManager.getSession(packet.getFrom());
  6.  
  7. try {
  8. // Invoke the interceptors before we process the read packet
  9. InterceptorManager.getInstance().invokeInterceptors(packet, session, true, false);
  10.  
  11. if (session == null || session.getStatus() == Session.STATUS_AUTHENTICATED) {
  12. JID recipientJID = packet.getTo();
  13.  
  14. ......
  15.  
  16. boolean isAcceptable = true;
  17. if (session instanceof LocalClientSession) {
  18. .....
  19. }
  20. if (isAcceptable) {
  21. boolean isPrivate = packet.getElement().element(QName.get("private", "urn:xmpp:carbons:2")) != null;
  22. try {
  23. // Deliver stanza to requested route
  24. routingTable.routePacket(recipientJID, packet, false);
  25. } catch (Exception e) {
  26. log.error("Failed to route packet: " + packet.toXML(), e);
  27. routingFailed(recipientJID, packet);
  28. }
  29. // Sent carbon copies to other resources of the sender:
  30. // When a client sends a <message/> of type "chat"
  31. if (packet.getType() == Message.Type.chat && !isPrivate && session != null) {
  32. List<JID> routes = routingTable.getRoutes(packet.getFrom().asBareJID(), null);
  33. for (JID route : routes) {
  34. if (!route.equals(session.getAddress())) {
  35. ClientSession clientSession = sessionManager.getSession(route);
  36. if (clientSession != null && clientSession.isMessageCarbonsEnabled()) {
  37. Message message = new Message();
  38. message.setType(packet.getType());
  39. message.setFrom(packet.getFrom().asBareJID());
  40. message.setTo(route);
  41. message.addExtension(new Sent(new Forwarded(packet)));
  42. clientSession.process(message);
  43. }
  44. }
  45. }
  46. }
  47. }
  48. }
  49. ......
  50. }

  其中,routingTable.routePacket(recipientJID, packet, false)是发送消息的关键代码。

  路由模块中,对消息的发送做了封装,在任何需要发送消息的地方,例如自定义插件中,只需要调用下面这个方法,就能完成消息的发送:

  1. XMPPServer.getInstance().getRoutingTable().routePacket(to, message, true);

  路由表中保存了该连接的Session对象,Session中携带有连接创建时生成的Connection对象,而从上一章我们知道,Connection是对MINA的Iosession的封装。

  换言之,其实路由表的消息发送功能,就是通过Connection调用MINA底层来实现的。答案是否是如此?下面来看看。

  路由表中的消息发送

  路由表中的其他细节,我们暂时不关注过多,目前主要看它的消息发送流程:

  消息发送的方法RoutingTableImpl.routePacket():

  1. @Override
  2. public void routePacket(JID jid, Packet packet, boolean fromServer) throws PacketException {
  3. boolean routed = false;
  4. try {
  5. if (serverName.equals(jid.getDomain())) {
  6. // Packet sent to our domain.
  7. routed = routeToLocalDomain(jid, packet, fromServer);
  8. }
  9. else if (jid.getDomain().endsWith(serverName) && hasComponentRoute(jid)) {
  10. // Packet sent to component hosted in this server
  11. routed = routeToComponent(jid, packet, routed);
  12. }
  13. else {
  14. // Packet sent to remote server
  15. routed = routeToRemoteDomain(jid, packet, routed);
  16. }
  17. } catch (Exception ex) {
  18.  
  19. Log.error("Primary packet routing failed", ex);
  20. }
  21.  
  22. if (!routed) {
  23. if (Log.isDebugEnabled()) {
  24. Log.debug("Failed to route packet to JID: {} packet: {}", jid, packet.toXML());
  25. }
  26. if (packet instanceof IQ) {
  27. iqRouter.routingFailed(jid, packet);
  28. }
  29. else if (packet instanceof Message) {
  30. messageRouter.routingFailed(jid, packet);
  31. }
  32. else if (packet instanceof Presence) {
  33. presenceRouter.routingFailed(jid, packet);
  34. }
  35. }
  36. }

  这里有几个分支:

  1. |-- routeToLocalDomain 路由到本地
  2. |-- routeToComponent 路由到组件
  3. |-- routeToRemoteDomain 路由到远程

  对于单机情况的消息,调用的是routeToLocalDomain()。

  RoutingTableImpl.routeToLocalDomain()

  1. private boolean routeToLocalDomain(JID jid, Packet packet, boolean fromServer) {
  2. boolean routed = false;
  3. Element privateElement = packet.getElement().element(QName.get("private", "urn:xmpp:carbons:2"));
  4. boolean isPrivate = privateElement != null;
  5. // The receiving server and SHOULD remove the <private/> element before delivering to the recipient.
  6. packet.getElement().remove(privateElement);
  7.  
  8. if (jid.getResource() == null) {
  9. // Packet sent to a bare JID of a user
  10. if (packet instanceof Message) {
  11. // Find best route of local user
  12. routed = routeToBareJID(jid, (Message) packet, isPrivate);
  13. }
  14. else {
  15. throw new PacketException("Cannot route packet of type IQ or Presence to bare JID: " + packet.toXML());
  16. }
  17. }
  18. else {
  19. // Packet sent to local user (full JID)
  20. ClientRoute clientRoute = usersCache.get(jid.toString());
  21. if (clientRoute == null) {
  22. clientRoute = anonymousUsersCache.get(jid.toString());
  23. }
  24. if (clientRoute != null) {
  25. if (!clientRoute.isAvailable() && routeOnlyAvailable(packet, fromServer) &&
  26. !presenceUpdateHandler.hasDirectPresence(packet.getTo(), packet.getFrom())) {
  27. Log.debug("Unable to route packet. Packet should only be sent to available sessions and the route is not available. {} ", packet.toXML());
  28. routed = false;
  29. } else {
  30. if (localRoutingTable.isLocalRoute(jid)) {
  31. if (packet instanceof Message) {
  32. Message message = (Message) packet;
  33. if (message.getType() == Message.Type.chat && !isPrivate) {
  34. List<JID> routes = getRoutes(jid.asBareJID(), null);
  35. for (JID route : routes) {
  36.  
  37. if (!route.equals(jid)) {
  38. ClientSession clientSession = getClientRoute(route);
  39. if (clientSession.isMessageCarbonsEnabled()) {
  40. Message carbon = new Message();
  41. // The wrapping message SHOULD maintain the same 'type' attribute value;
  42. carbon.setType(message.getType());
  43. // the 'from' attribute MUST be the Carbons-enabled user's bare JID
  44. carbon.setFrom(route.asBareJID());
  45. // and the 'to' attribute MUST be the full JID of the resource receiving the copy
  46. carbon.setTo(route);
  47. carbon.addExtension(new Received(new Forwarded(message)));
  48.  
  49. try {
  50. localRoutingTable.getRoute(route.toString()).process(carbon);
  51. } catch (UnauthorizedException e) {
  52. Log.error("Unable to route packet " + packet.toXML(), e);
  53. }
  54. }
  55. }
  56. }
  57. }
  58. }
  59.  
  60. // This is a route to a local user hosted in this node
  61. try {
  62. localRoutingTable.getRoute(jid.toString()).process(packet);
  63. routed = true;
  64. } catch (UnauthorizedException e) {
  65. Log.error("Unable to route packet " + packet.toXML(), e);
  66. }
  67. }
  68. else {
  69. // This is a route to a local user hosted in other node
  70. if (remotePacketRouter != null) {
  71. routed = remotePacketRouter
  72. .routePacket(clientRoute.getNodeID().toByteArray(), jid, packet);
  73. if (!routed) {
  74. removeClientRoute(jid); // drop invalid client route
  75. }
  76. }
  77. }
  78. }
  79. }
  80. }
  81. return routed;
  82. }

  上面的关键代码中是这一段:

  1. try {
  2. localRoutingTable.getRoute(route.toString()).process(carbon);
  3. } catch (UnauthorizedException e) {
  4. Log.error("Unable to route packet " + packet.toXML(), e);
  5. }

  可以看出,RoutingTable的路由功能,是通过localRoutingTable实现的。

  LocalRoutingTable中用一个容器保存了所有的路由:

  1. Map<String, RoutableChannelHandler> routes = new ConcurrentHashMap<>();

  RoutingTableImpl中通过调用LocalRoutingTable的add、get、remove等方法,实现对路由的管理。

  localRoutingTable.getRoute()方法实现从路由表中获取RoutableChannelHandler对象,那么具体消息是如何通过路由发出去的?

  要解释这个问题,先来看一下与RoutableChannelHandler相关的继承和派生关系,如下:

  1. |-- ChannelHandler
  2. |-- RoutableChannelHandler
  3. |-- Session
  4. |-- LocalSession
  5. |-- LocalClientSession

  也就是说,其实localRoutingTable.getRoute(route.toString()).process(carbon)最终调用的是LacalSession.process()。

  LacalSession.process()代码如下:

  1. @Override
  2. public void process(Packet packet) {
  3. // Check that the requested packet can be processed
  4. if (canProcess(packet)) {
  5. // Perform the actual processing of the packet. This usually implies sending
  6. // the packet to the entity
  7. try {
  8.  
  9. InterceptorManager.getInstance().invokeInterceptors(packet, this, false, false);
  10.  
  11. deliver(packet);
  12.  
  13. InterceptorManager.getInstance().invokeInterceptors(packet, this, false, true);
  14. }
  15. catch (PacketRejectedException e) {
  16. // An interceptor rejected the packet so do nothing
  17. }
  18. catch (Exception e) {
  19. Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
  20. }
  21. }
  22. ......
  23. }

  其中的deliver()是LacalSession定义的一个插象方法,由其子类来实现。

  有一点值得提一下,在deliver()前后,都做了拦截,方便在发送的前后做一些额外的处理。

  继续讲回deliver(),对于C2S连接类型来说,它是在LocalClientSession类中实现。

  LocalClientSession.deliver()代码如下:

  1. @Override
  2. public void deliver(Packet packet) throws UnauthorizedException {
  3.  
  4. conn.deliver(packet);
  5. streamManager.sentStanza(packet);
  6. }

  此时的发送方法conn.deliver()中的conn,就是来自最初在sessionOpened()被调用时创建的NIOConnection对象。

  NIOConnection.deliver():

  1. @Override
  2. public void deliver(Packet packet) throws UnauthorizedException {
  3. if (isClosed()) {
  4. backupDeliverer.deliver(packet);
  5. }
  6. else {
  7. boolean errorDelivering = false;
  8. IoBuffer buffer = IoBuffer.allocate(4096);
  9. buffer.setAutoExpand(true);
  10. try {
  11. buffer.putString(packet.getElement().asXML(), encoder.get());
  12. if (flashClient) {
  13. buffer.put((byte) '\0');
  14. }
  15. buffer.flip();
  16.  
  17. ioSessionLock.lock();
  18. try {
  19. ioSession.write(buffer);
  20. } finally {
  21. ioSessionLock.unlock();
  22. }
  23. }
  24. catch (Exception e) {
  25. Log.debug("Error delivering packet:\n" + packet, e);
  26. errorDelivering = true;
  27. }
  28. if (errorDelivering) {
  29. close();
  30. // Retry sending the packet again. Most probably if the packet is a
  31. // Message it will be stored offline
  32. backupDeliverer.deliver(packet);
  33. }
  34. else {
  35. session.incrementServerPacketCount();
  36. }
  37. }
  38. }

  NIOConnection.deliver()中,通过其内部包装的IoSession对象,调用write()方法将数据流写入网卡中,完成消息的发送。

  ConnectionHandler.messageSent()

  消息发送完成,MINA回调:

  1. @Override
  2. public void messageSent(IoSession session, Object message) throws Exception {
  3. super.messageSent(session, message);
  4. // Update counter of written btyes
  5. updateWrittenBytesCounter(session);
  6.  
  7. System.out.println("Fordestiny-SEND: "+ioBufferToString(message));
  8. }

  至此,系统完成了一条消息的接收、转发。


  其实消息的路由中,除了消息的整个流通路径之外,怎么保证消息能够准确的发送到对应的客户端是至关重要的。这方面Openfire是如何处理的,在下个章节做解析,即Openfire的会话管理和路由表。Over!

即时通信系统Openfire分析之四:消息路由的更多相关文章

  1. 即时通信系统Openfire分析之五:会话管理

    什么是会话? A拨了B的电话 电话接通 A问道:Are you OK? B回复:I have a bug! A挂了电话 这整个过程就是会话. 会话(Session)是一个客户与服务器之间的不中断的请求 ...

  2. 即时通信系统Openfire分析之六:路由表 RoutingTable

    还是从会话管理说起 上一章,Session经过预创建.认证之后,才正常可用.认证时,最重要的操作,就是将Session加入到路由表,使之拥用了通信功能. 添加到至路由表的操作,是在SessionMan ...

  3. 即时通信系统Openfire分析之八:集群管理

    前言 在第六章<路由表>中,客户端进行会话时,首先要获取对方的Session实例.获取Session实例的方法,是先查找本地路由表,若找不到,则通过路由表中的缓存数据,由定位器获取. 路由 ...

  4. 即时通信系统Openfire分析之二:主干程序分析

    引言 宇宙大爆炸,于是开始了万物生衍,从一个连人渣都还没有的时代,一步步进化到如今的花花世界. 然而沧海桑田,一百多亿年过去了…. 好复杂,但程序就简单多了,main()函数运行,敲个回车,一行Hel ...

  5. 即时通信系统Openfire分析之一:Openfire与XMPP协议

     引言 目前互联网产品使用的即时通信协议有这几种:即时信息和空间协议(IMPP).空间和即时信息协议(PRIM).针对即时通讯和空间平衡扩充的进程开始协议SIP(SIMPLE)以及XMPP.PRIM与 ...

  6. 即时通信系统Openfire分析之七:集群配置

    前言 写这章之前,我犹豫了一会.在这个时候提集群,从章节安排上来讲,是否合适?但想到上一章<路由表>的相关内容,应该不至于太突兀.既然这样,那就撸起袖子干吧. Openfire的单机并发量 ...

  7. 即时通信系统Openfire分析之三:ConnectionManager 连接管理

    Openfire是怎么实现连接请求的? XMPPServer.start()方法,完成Openfire的启动.但是,XMPPServer.start()方法中,并没有提及如何监听端口,那么Openfi ...

  8. 即时通信系统中实现聊天消息加密,让通信更安全【低调赠送:C#开源即时通讯系统(支持广域网)——GGTalk4.5 最新源码】

    在即时通讯系统(IM)中,加密重要的通信消息,是一个常见的需求.尤其在一些政府部门的即时通信软件中(如税务系统),对即时聊天消息进行加密是非常重要的一个功能,因为谈话中可能会涉及到机密的数据.我在最新 ...

  9. 即时通信系统中如何实现:聊天消息加密,让通信更安全? 【低调赠送:QQ高仿版GG 4.5 最新源码】

    加密重要的通信消息,是一个常见的需求.在一些政府部门的即时通信软件中(如税务系统),对聊天消息进行加密是非常重要的一个功能,因为谈话中可能会涉及到机密的数据.我在最新的GG 4.5中,增加了对聊天消息 ...

随机推荐

  1. MVC 路由设置伪静态

    public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/ ...

  2. AJAX 中JSON 和JSONP 的区别 以及请求原理

    AJAX 跨域请求 - JSONP获取JSON数据 博客分类: Javascript /Jquery / Bootstrap / Web   Asynchronous JavaScript and X ...

  3. 阿里云有对手了!CDN横评:腾讯云优势明显

    如今,云计算产品越来越多,像国内的BAT三大巨头都提供了云主机(腾讯云CVM.阿里云ECS.百度云BCC),另外还有存储.数据库.安全等相关云服务.在这其中,CDN也是一项重要的云服务,CDN指的是内 ...

  4. 初学Python(一)——数据类型

    初学Python(一)——数据类型 初学Python,主要整理一些学习到的知识点,这次是数据类型. #-*- coding:utf-8 -*- #整数 print 1 #浮点数=小数 print 1. ...

  5. CVTE后台开发实习生岗位面试经验(2017.3)

    3月份我在看准网发布过这篇面经,现在转过来.原文链接:http://www.kanzhun.com/gsmsh10433357.html 投递岗位是web后台实习生 做完笔试后一天对方即发来面试通知 ...

  6. Numpy 操作

    一.Numpy 属性 # 列表转化为矩阵 In []: arr = np.array([[,,],[,,]]) In []: arr Out[]: array([[, , ], [, , ]]) 1, ...

  7. Python 爬虫抓取代理IP,并检测联通性

    帮朋友抓了一些代理IP,并根据测试联的通性,放在了不通的文件夹下.特将源码分享 注意: 1,环境Python3.5 2,安装BeautifulSoup4  requests 代码如下: 1 2 3 4 ...

  8. Hibernate中HQL函数汇总及获取当前时间进行比较举例

    在很多时候,我们负责的项目中,在数据访问层(DAO层)通常我们会使用sql语句或者hql语句,而在我们使用hql语句拼接时有时会报错,通常的原因是:我们使用了标准的sql语句,开启的确是hiberna ...

  9. Linux下ftp的安装配置

    1.查看ftp包是否可用yum list | grep vsftpd 2.安装ftpyum install vsftpd 3.启动systemctl start vsftpd 4.开机启动chkcon ...

  10. 手脱UPX(堆栈平衡原理)

    一开始看到pushad F8执行直到只有esp,eip,变化 在esp处follow in dump 下硬件访问断点 F9运行在硬件断点停下 到达一个长跳转(跳到OEP) 完成 ----------- ...