Thrift结构分析及增加取客户端IP功能实现
目录
1. 前言
分析Thrift的结构动机是为了实现服务端能取到客户端的IP,因此需要对它的结构、调用流程有些了解。另外,请注意本文针对的是TNonblockingServer,不包含TThreadPoolServer、TThreadedServer和TSimpleServer。
thrift对网络连接没有使用内存池,最直接简单的性能优化是绑定Google gperftools中的TCMalloc。
2. 示例Service
service EchoService { string hello(1: string greetings); } class EchoHandler: public EchoServiceIf { private: virtual void hello(std::string& _return, const std::string& greetings); }; |
3. 网络部分类图
Thrift线程模型为若干IO线程TNonblockingIOThread(负责收发TCP连接的数据),以及主线程(负责监听TCP连接及接受连接请求)组成。
主线程不一定就是进程的主线程,哪个线程调用了TServer::run()或TServer::serve()就是本文所说的主线程。就当前最新版本(0.9.2)的Thrift而言,调用TServer::run()或TServer::serve()均可以,原因是TServer::run()除无条件的调用外TServer::serve(),没有做任何其它事。对TServer::serve()的调用实际是对TServer的实现类TNonblockingServer的serve()的调用。
简而言之,TNonblockingIOThread负责数据的收发,而TNonblockingServer负责接受连接请求。
在使用中需要注意,调用TServer::run()或TServer::serve()的线程或进程会被阻塞,阻塞进入libevent的死循环,Linux上是死循环调用epoll_wait()。
4. 线程模式
Thrift将线程分成两类:
4.1. IO线程
IO线程负责监听和接受连接请求,和接收客户端发送过来的数据,收到完整请求后,以Task方式传递给工作线程,由工作线程回调。
IO线程针对TNonblockingServer,TNonblockingServer提供方法setNumIOThreads()来设置IO线程个数。第一个IO线程总是独占调用TServer::server()或TServer::run()的线程。
IO线程在accept一个连接后,会创建一个TConnection实例(在TNonblockingServer::TConnection::transition()中),而TConnection会创建一个Task(在TNonblockingServer::TConnection::transition()中完成),由TNonblockingServer将Task传递给ThreadManager。
纠正:上图中的TNonblockingServer应当为TNonblockingIOThread。
注意函数TNonblockingServer::handleEvent()的下小段代码,getIOThreadNumber()并不是表示取得IO线程个数,而是该线程在线程组中的ID,可以这么认为等于0时表示0号线程:
void TNonblockingServer::handleEvent(int fd, short which) { if (clientConnection->getIOThreadNumber() == 0) { clientConnection->transition(); } else { clientConnection->notifyIOThread(); // 最终也会调用transition() } } |
4.2. 工作线程
工作线程负责回调和对客户端响应。
4.2.1. 工作线程类图
4.2.2. 工作线程启动过程
5. 一个RPC函数被调用时序图
5.1. 启动准备
准备的工作包括:
1) 启动监听连接
2) 启动收发数据线程
3) 初始化运行环境
在这里,可以看到第一次对TServerEventHandler的回调:
5.2. 接受连接
从接受连接的时序过程可以看出:在该连接TConnection接收数据之前,先调用了TServerEventHandler::createContext(),这个就是获取客户端IP的机会之一,但是当前的实现没有将相关的信息作为参数传递给TServerEventHandler::createContext()。
5.3. 收发数据:执行调用
这过程中对TServerEventHandler::processContext(connectionContext_, getTSocket())进行了回调,并传递了TSocket。
5.4. 服务端回调代码解读
下面是thrift编译生成的代码片段,为服务端的代码:
// TProtocol为协议接口,常用实现类为TBinaryProtocol等 void EchoServiceProcessor::process_hello(int32_t seqid, // 消息序列号 ::apache::thrift::protocol::TProtocol* iprot, // 输入参数 ::apache::thrift::protocol::TProtocol* oprot, // 输出参数 void* callContext) { // eventHandler_类型为TProcessorEventHandler,是一个被回调对象 void* ctx = NULL; if (this->eventHandler_.get() != NULL) { ctx = this->eventHandler_->getContext("EchoService.hello", callContext); } ::apache::thrift::TProcessorContextFreer freer(this->eventHandler_.get(), ctx, "EchoService.hello"); if (this->eventHandler_.get() != NULL) { this->eventHandler_->preRead(ctx, "HelloService.hello"); // 回调TProcessorEventHandler } EchoService_hello_args args; // 输入参数 args.read(iprot); // 反序列化输入参数 iprot->readMessageEnd(); uint32_t bytes = iprot->getTransport()->readEnd(); if (this->eventHandler_.get() != NULL) { this->eventHandler_->postRead(ctx, "EchoService.hello", bytes); // 回调TProcessorEventHandler } // EchoService_hello_result是thrift编译生成的类 EchoService_hello_result result; // 输出参数,也就是thrift文件中定义的返回值 try { iface_->hello(result.success, args.greetings); // 这里就是回调用户自己写的代码了 result.__isset.success = true; } catch (const std::exception& e) { if (this->eventHandler_.get() != NULL) { this->eventHandler_->handlerError(ctx, "EchoService.hello"); // 回调TProcessorEventHandler } // 下段是异常时的返回,客户端应当catch它 ::apache::thrift::TApplicationException x(e.what()); // writeMessageBegin序列化消息头 oprot->writeMessageBegin("hello", ::apache::thrift::protocol::T_EXCEPTION, seqid); x.write(oprot); // 将x序列化到oprot oprot->writeMessageEnd(); oprot->getTransport()->writeEnd(); oprot->getTransport()->flush(); return; } if (this->eventHandler_.get() != NULL) { this->eventHandler_->preWrite(ctx, "EchoService.hello"); // 回调TProcessorEventHandler } // 下段为序列化输出参数,也注是返回值啦 oprot->writeMessageBegin("hello", ::apache::thrift::protocol::T_REPLY, seqid); // 序列化消息头 result.write(oprot); // 序列化result到oprot oprot->writeMessageEnd(); bytes = oprot->getTransport()->writeEnd(); oprot->getTransport()->flush(); if (this->eventHandler_.get() != NULL) { this->eventHandler_->postWrite(ctx, "EchoService.hello", bytes); // 回调TProcessorEventHandler } } |
5.5. 客户端回调代码解读
下面是thrift编译生成的代码片段,为客户端的代码:
// 同步调用实现 // hello就是客户端直接调用的 void EchoServiceClient::hello(std::string& _return, const std::string& greetings) { send_hello(greetings); // 序列化输入参数,并发送给服务端 recv_hello(_return); // 接收服务端的返回,并反序列化 } // 向服务端发起调用 void EchoServiceClient::send_hello(const std::string& greetings) { int32_t cseqid = 0; oprot_->writeMessageBegin("hello", ::apache::thrift::protocol::T_CALL, cseqid); // 类EchoService_hello_pargs也是thrift编译生成的类,所有的参数都是它的数据成员 EchoService_hello_pargs args; args.greetings = &greetings; args.write(oprot_); // 序列化 oprot_->writeMessageEnd(); oprot_->getTransport()->writeEnd(); oprot_->getTransport()->flush(); } // 接收服务端的响应 void EchoServiceClient::recv_hello(std::string& _return) { int32_t rseqid = 0; std::string fname; // 函数名 ::apache::thrift::protocol::TMessageType mtype; iprot_->readMessageBegin(fname, mtype, rseqid); if (mtype == ::apache::thrift::protocol::T_EXCEPTION) { ::apache::thrift::TApplicationException x; x.read(iprot_); iprot_->readMessageEnd(); iprot_->getTransport()->readEnd(); throw x; // 抛出异常 } if (mtype != ::apache::thrift::protocol::T_REPLY) { iprot_->skip(::apache::thrift::protocol::T_STRUCT); iprot_->readMessageEnd(); iprot_->getTransport()->readEnd(); } if (fname.compare("hello") != 0) { iprot_->skip(::apache::thrift::protocol::T_STRUCT); iprot_->readMessageEnd(); iprot_->getTransport()->readEnd(); } EchoService_hello_presult result; result.success = &_return; result.read(iprot_); // 反序列化 iprot_->readMessageEnd(); iprot_->getTransport()->readEnd(); if (result.__isset.success) { // _return pointer has now been filled return; } throw ::apache::thrift::TApplicationException(::apache::thrift::TApplicationException::MISSING_RESULT, "hello failed: unknown result"); } |
5.6. 服务端dispatchCall的实现
thrift编译生成的类Ec hoServiceProcessor,实现了接口apache::thrift::TDispatchProcessor的dispatchCall()方法:
bool EchoServiceProcessor::dispatchCall(::apache::thrift::protocol::TProtocol* iprot, // 输入参数 ::apache::thrift::protocol::TProtocol* oprot, // 输出参数 const std::string& fname, // 被调用的函数名 int32_t seqid, // 序列号 void* callContext) { ProcessMap::iterator pfn; // typedef void (EchoServiceProcessor::*ProcessFunction)(int32_t, // ::apache::thrift::protocol::TProtocol*, // ::apache::thrift::protocol::TProtocol*, // void*); // typedef std::map<std::string, ProcessFunction> ProcessMap; pfn = processMap_.find(fname); // 根据函数名,找到函数(ProcessMap processMap_;) if (pfn == processMap_.end()) { // 没有找到时,抛出异常 iprot->skip(::apache::thrift::protocol::T_STRUCT); iprot->readMessageEnd(); iprot->getTransport()->readEnd(); ::apache::thrift::TApplicationException x(::apache::thrift::TApplicationException::UNKNOWN_METHOD, "Invalid method name: '"+fname+"'"); oprot->writeMessageBegin(fname, ::apache::thrift::protocol::T_EXCEPTION, seqid); x.write(oprot); oprot->writeMessageEnd(); // 序列化后,调用了Transport,而Transport调用了网络send oprot->getTransport()->writeEnd(); oprot->getTransport()->flush(); return true; } // 找到,则进行回调 (this->*(pfn->second))(seqid, iprot, oprot, callContext); return true; } |
6. TProtocol
TProtocol提供序列化和反序列化能力,定义了消息包的编码和解码协议,它的实现有以下几种:
1) TBinaryProtocol 二进制编解码
2) TDebugProtocol 用于调试的,可读的文本编解码
3) TJSONProtocol 基于json的编解码
4) TCompactProtocol 压缩的二进制编解码
如果需要为thrift增加一种数据类型,则需要修改TProtocol,增加对新数据类型的序列化和反序列化实现。
7. TTransport
TTransport负责收发数据,可以简单的是对Socket的包装,但是也支持非Socket,比如Pipe。其中TSocket为TServerSocket使用的Transport。
8. TProtocol&TTransport
对于TNonblockingServer默认使用的是输入和输出Transport,都是以TMemoryBuffer为TTransport。
TProtocol本身没有缓冲区等,它只是序列化和反序列化。然而它依赖于TTransport,通过TTransport发送数据。以TBinaryProtocol为例:
// 序列化int16_t值 template <class Transport_> uint32_t TBinary(const int16_t i16) { int16_t net = (int16_t)htons(i16); this->trans_->write((uint8_t*)&net, 2); // 看到没?这里调用的是TTransport return 2; } |
对比看下TTransport::write的实现:
// TSocket是一种TTransport void TSocket::write(const uint8_t* buf, uint32_t len) { uint32_t sent = 0; // 从下面的实现可以看出发送是同步的 while (sent < len) { uint32_t b = write_partial(buf + sent, len - sent); // 这里实际调用的是系统的send() if (b == 0) { // This should only happen if the timeout set with SO_SNDTIMEO expired. // Raise an exception. throw TTransportException(TTransportException::TIMED_OUT, "send timeout expired"); } sent += b; } } |
9. 数据流向关系
客户端发送数据时,会触发libevent事件,然后调用Transport收数据。包完整后,调用Protocol反序列化,接着就调用服务端的代码。
前半部分在IO线程中完成,后半部分在工作线程中完成。
10. 取客户端IP
为取得客户端的IP,有三个办法:
1) 网上博文http://blog.csdn.net/hbuxiaoshe/article/details/38942869介绍的方法也是可行的,不过让人有些纠结;
2) 修改Thrift的实现,为TServerEventHandler::createContext()增加一个参数,将TSocket作为参数传递,这样就可以非常轻易的取得客户端的IP了。最简单的修改为:
class TServerEventHandler { public: virtual void* createContext(boost::shared_ptr<TProtocol> input, boost::shared_ptr<TProtocol> output, TTransport* transport); // 对于TNonblockingServer实际传递为TSocket }; |
3) 不修改Thrift的实现。
在“收发数据:执行调用”的流程中,可以发现有对TServerEventHandler::processContext()的调用,而这里真好将TSocket作为第二个参数进行了传递,因此可以直接利用。
TServerEventHandler::createContext()和TServerEventHandler::processContext()的不同在于:前者只在建立连接时被调用一次,而后者每一个RPC调用时都会调用一次。
#ifndef MOOON_NET_THRIFT_HELPER_H #define MOOON_NET_THRIFT_HELPER_H #include <mooon/net/config.h> #include <mooon/sys/log.h> #include <mooon/utils/scoped_ptr.h> #include <arpa/inet.h> #include <boost/scoped_ptr.hpp> #include <thrift/concurrency/PosixThreadFactory.h> #include <thrift/concurrency/ThreadManager.h> #include <thrift/protocol/TBinaryProtocol.h> #include <thrift/server/TNonblockingServer.h> #include <thrift/transport/TSocketPool.h> #include <thrift/transport/TTransportException.h> NET_NAMESPACE_BEGIN // 用来判断thrift是否已经连接,包括两种情况: // 1.从未连接过,也就是还未打开过连接 // 2.连接被对端关闭了 inline bool thrift_not_connected( apache::thrift::transport::TTransportException::TTransportExceptionType type) { return (apache::thrift::transport::TTransportException::NOT_OPEN == type) || (apache::thrift::transport::TTransportException::END_OF_FILE == type); } inline bool thrift_not_connected( apache::thrift::transport::TTransportException& ex) { apache::thrift::transport::TTransportException::TTransportExceptionType type = ex.getType(); return thrift_not_connected(type); } // thrift客户端辅助类 // // 使用示例: // mooon::net::CThriftClientHelper<ExampleServiceClient> client(rpc_server_ip, rpc_server_port); // try // { // client.connect(); // client->foo(); // } // catch (apache::thrift::transport::TTransportException& transport_ex) // { // MYLOG_ERROR("thrift exception: %s\n", transport_ex.what()); // } // catch (apache::thrift::transport::TApplicationException& app_ex) // { // MYLOG_ERROR("thrift exception: %s\n", app_ex.what()); // } // catch (apache::thrift::TException& tx) // { // MYLOG_ERROR("thrift exception: %s\n", tx.what()); // } // Transport除默认的TFramedTransport (TBufferTransports.h),还可选择: // TBufferedTransport (TBufferTransports.h) // THttpTransport // TZlibTransport // TFDTransport (TSimpleFileTransport) // // Protocol除默认的apache::thrift::protocol::TBinaryProtocol,还可选择: // TCompactProtocol // TJSONProtocol // TDebugProtocol template <class ThriftClient, class Protocol=apache::thrift::protocol::TBinaryProtocol, class Transport=apache::thrift::transport::TFramedTransport> class CThriftClientHelper { public: // host thrift服务端的IP地址 // port thrift服务端的端口号 // connect_timeout_milliseconds 连接thrift服务端的超时毫秒数 // receive_timeout_milliseconds 接收thrift服务端发过来的数据的超时毫秒数 // send_timeout_milliseconds 向thrift服务端发送数据时的超时毫秒数 CThriftClientHelper(const std::string &host, uint16_t port, int connect_timeout_milliseconds=2000, int receive_timeout_milliseconds=2000, int send_timeout_milliseconds=2000); ~CThriftClientHelper(); // 连接thrift服务端 // // 出错时,可抛出以下几个thrift异常: // apache::thrift::transport::TTransportException // apache::thrift::TApplicationException // apache::thrift::TException void connect(); // 断开与thrift服务端的连接 // // 出错时,可抛出以下几个thrift异常: // apache::thrift::transport::TTransportException // apache::thrift::TApplicationException // apache::thrift::TException void close(); ThriftClient* get() { return _client.get(); } ThriftClient* get() const { return _client.get(); } ThriftClient* operator ->() { return get(); } ThriftClient* operator ->() const { return get(); } const std::string& get_host() const { return _host; } uint16_t get_port() const { return _port; } private: std::string _host; uint16_t _port; boost::shared_ptr<apache::thrift::transport::TSocketPool> _sock_pool; boost::shared_ptr<apache::thrift::transport::TTransport> _socket; boost::shared_ptr<apache::thrift::transport::TFramedTransport> _transport; boost::shared_ptr<apache::thrift::protocol::TProtocol> _protocol; boost::shared_ptr<ThriftClient> _client; }; //////////////////////////////////////////////////////////////////////////////// // thrift服务端辅助类 // // 使用示例: // mooon::net::CThriftServerHelper<CExampleHandler, ExampleServiceProcessor> _thrift_server; // try // { // _thrift_server.serve(listen_port); // } // catch (apache::thrift::TException& tx) // { // MYLOG_ERROR("thrift exception: %s\n", tx.what()); // } // ProtocolFactory除了默认的TBinaryProtocolFactory,还可选择: // TCompactProtocolFactory // TJSONProtocolFactory // TDebugProtocolFactory // // Server除默认的TNonblockingServer外,还可选择: // TSimpleServer // TThreadedServer // TThreadPoolServer template <class ThriftHandler, class ServiceProcessor, class ProtocolFactory=apache::thrift::protocol::TBinaryProtocolFactory, class Server=apache::thrift::server::TNonblockingServer> class CThriftServerHelper { public: // 启动rpc服务,请注意该调用是同步阻塞的,所以需放最后调用 // port thrift服务端的监听端口号 // num_threads thrift服务端开启的线程数 // // 出错时,可抛出以下几个thrift异常: // apache::thrift::transport::TTransportException // apache::thrift::TApplicationException // apache::thrift::TException // 参数num_io_threads,只有当Server为TNonblockingServer才有效 void serve(uint16_t port, uint8_t num_worker_threads=1, uint8_t num_io_threads=1); void serve(const std::string &ip, uint16_t port, uint8_t num_worker_threads, uint8_t num_io_threads=1); void stop(); private: boost::shared_ptr<ThriftHandler> _handler; boost::shared_ptr<apache::thrift::TProcessor> _processor; boost::shared_ptr<apache::thrift::protocol::TProtocolFactory> _protocol_factory; boost::shared_ptr<apache::thrift::server::ThreadManager> _thread_manager; boost::shared_ptr<apache::thrift::concurrency::PosixThreadFactory> _thread_factory; boost::shared_ptr<apache::thrift::server::TServer> _server; }; //////////////////////////////////////////////////////////////////////////////// // 被thrift回调的写日志函数,由set_thrift_log_write_function()调用它 inline void write_log_function(const char* log) { MYLOG_INFO("%s", log); } // 将thrift输出写入到日志文件中 inline void set_thrift_log_write_function() { if (log != NULL) { apache::thrift::GlobalOutput.setOutputFunction(write_log_function); } } //////////////////////////////////////////////////////////////////////////////// template <class ThriftClient, class Protocol, class Transport> CThriftClientHelper<ThriftClient, Protocol, Transport>::CThriftClientHelper( const std::string &host, uint16_t port, int connect_timeout_milliseconds, int receive_timeout_milliseconds, int send_timeout_milliseconds) : _host(host) , _port(port) { set_thrift_log_write_function(); _sock_pool.reset(new apache::thrift::transport::TSocketPool()); _sock_pool->addServer(host, (int)port); _sock_pool->setConnTimeout(connect_timeout_milliseconds); _sock_pool->setRecvTimeout(receive_timeout_milliseconds); _sock_pool->setSendTimeout(send_timeout_milliseconds); _socket = _sock_pool; // Transport默认为apache::thrift::transport::TFramedTransport _transport.reset(new Transport(_socket)); // Protocol默认为apache::thrift::protocol::TBinaryProtocol _protocol.reset(new Protocol(_transport)); _client.reset(new ThriftClient(_protocol)); } template <class ThriftClient, class Protocol, class Transport> CThriftClientHelper<ThriftClient, Protocol, Transport>::~CThriftClientHelper() { close(); } template <class ThriftClient, class Protocol, class Transport> void CThriftClientHelper<ThriftClient, Protocol, Transport>::connect() { if (!_transport->isOpen()) { _transport->open(); } } template <class ThriftClient, class Protocol, class Transport> void CThriftClientHelper<ThriftClient, Protocol, Transport>::close() { if (_transport->isOpen()) { _transport->close(); } } //////////////////////////////////////////////////////////////////////////////// template <class ThriftHandler, class ServiceProcessor, class ProtocolFactory, class Server> void CThriftServerHelper<ThriftHandler, ServiceProcessor, ProtocolFactory, Server>::serve(uint16_t port, uint8_t num_worker_threads, uint8_t num_io_threads) { serve("0.0.0.0", port, num_worker_threads, num_io_threads); } template <class ThriftHandler, class ServiceProcessor, class ProtocolFactory, class Server> void CThriftServerHelper<ThriftHandler, ServiceProcessor, ProtocolFactory, Server>::serve(const std::string &ip, uint16_t port, uint8_t num_worker_threads, uint8_t num_io_threads) { set_thrift_log_write_function(); _handler.reset(new ThriftHandler); _processor.reset(new ServiceProcessor(_handler)); // ProtocolFactory默认为apache::thrift::protocol::TBinaryProtocolFactory _protocol_factory.reset(new ProtocolFactory()); _thread_manager = apache::thrift::server::ThreadManager::newSimpleThreadManager(num_worker_threads); _thread_factory.reset(new apache::thrift::concurrency::PosixThreadFactory()); _thread_manager->threadFactory(_thread_factory); _thread_manager->start(); // Server默认为apache::thrift::server::TNonblockingServer Server* server = new Server(_processor, _protocol_factory, port, _thread_manager); if (sizeof(Server) == sizeof(apache::thrift::server::TNonblockingServer)) server->setNumIOThreads(num_io_threads); _server.reset(server); _server->run(); // 这里也可直接调用serve(),但推荐run() } template <class ThriftHandler, class ServiceProcessor, class ProtocolFactory, class Server> void CThriftServerHelper<ThriftHandler, ServiceProcessor, ProtocolFactory, Server>::stop() { _server->stop(); } NET_NAMESPACE_END #endif // MOOON_NET_THRIFT_HELPER_H |
11. 日志输出
默认thrift日志打屏,但其实可以让它输出到自己的日志文件中。这个功能通过全局对象apache::thrift::GlobalOutput来实现,在Thrift.h中声明了GlobalOutput,它的定义在Thrift.cpp文件中。
类TOutput提供了方法setOutputFunction()用来设置日志输出函数:
class TOutput{ public: inline void setOutputFunction(void (*function)(const char *)); }; |
调用setOutputFunction()设置回调函数,即可将日志输出到自己的日志文件中,遗憾的是不能自动区分日志级别。更佳的做法是定义一个抽象接口,然后让使用者注入接口实现,如mooon中ILogger:
https://github.com/eyjian/mooon/blob/master/common_library/include/mooon/sys/log.h。
具体做法,可以参考:https://github.com/eyjian/mooon/blob/master/common_library/include/mooon/net/thrift_helper.h。
附:问题
如何让Thrift只在指定的IP上监听,而不是监听0.0.0.0?
Thrift结构分析及增加取客户端IP功能实现的更多相关文章
- Nginx作为反向代理时传递客户端IP的设置方法
因为nginx的优越性,现在越来越多的用户在生产环境中使用nginx作为前端,不管nginx在前端是做负载均衡还是只做简单的反向代理,都需要把日志转发到后端real server,以方便我们检查程序的 ...
- NGINX前端代理TOMCAT取真实客户端IP
nginx前端代理tomcat取真实客户端IP 使用Nginx作为反向代理时,Tomcat的日志记录的客户端IP就不在是真实的客户端IP,而是Nginx代理的IP.要解决这个问题可以在Nginx配置一 ...
- C#服务器获取客户端IP地址以及归属地探秘
背景:博主本是一位Windows桌面应用程序开发工程师,对网络通信一知半解.一日老婆逛完某宝,问:"为什么他们知道我的地址呢,他们是怎么获取我的地址的呢?" 顺着这个问题我们的探秘 ...
- JAVA获取客户端IP地址
在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的.但是在通过了Apache,Squid等反向代理软件就不能获取到客户端的真实I ...
- java服务器获取客户端ip
在写服务端代码时,有时需要对客户端ip做认证,比如限制只有某些ip能访问,或者1个ip1天只能访问几次.最近就碰到个需要限制ip的情况,从网上找了一些服务器获取客户端ip的方法,说的都不太完善,这里整 ...
- 根据Request获取客户端IP
转自: http://www.cnblogs.com/icerainsoft/p/3584532.html http://www.cnblogs.com/bingya/articles/3134227 ...
- Struts如何获取客户端ip地址
在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的.但是在通过了Apache,Squid等反向代理软件就不能获取到客户端的真实I ...
- python中通过客户端IP拿到所在城市和当地天气信息—附带项目案例
熟悉老一代QQ的小伙伴可能都知道,很早以前的QQ,鼠标滑到头像的位置,你的位置和IP会在详情页显示,那么这个是如何做到的呢?下面我们就来玩一玩这个东西 首先,需求分析: 1.拿到客户端IP 2.通过I ...
- Tawk.to一键给自己的网站增加在线客服功能
Tawk.to一键给自己的网站增加在线客服功能 很多外贸网站只有contact页面,留下邮箱.电话等联系方式,而在国际贸易当中能够及时在线交流沟通,能给客户留下更好的印象.接下来,就让我们一起来了解一 ...
随机推荐
- CentOS7修复python拯救yum - 转载
原文:http://blog.51cto.com/welcomeweb/2132654 本人正在吹着空调,喝着茶水,然后qq头像抖了两下,业务开发同学给我打了个招呼,“忙么?帮个忙可以不?” 这很明显 ...
- RabbitMQ内存爆出问题解决思路
http://www.bubuko.com/infodetail-2121050.html RabbitMQ升级到3.6.1版本后,随着业务和系统功能的增加,出现RabbitMQ内存陡增直至服务宕掉的 ...
- Dubbo 基础教程
原文地址:Dubbo 基础教程 博客地址:http://www.extlight.com 一.前言 当服务越来越多时,容量的评估,小服务资源的浪费等问题逐渐显现,此时需要增加一个调度中心基于访问压力实 ...
- SpringCloud初体验:四、API GateWay 服务网关
网关服务很多,比如:Zuul.Kong.spring cloud gateway ……, 这里不纠结哪种性能好,本次体验是用的 spring cloud gateway 更多网关比较可以了解这篇文章: ...
- PHP 通过实现 Iterator(迭代器)接口来读取大文件文本
读了NGINX的access日志,bnb_manage_access.log(31M) 和 bnb_wechat_access.log(50M) 附上代码: <?php /** * User: ...
- HR-人力资源管理系统(Human Resources Management System,HRMS)
人力资源管理系统(Human Resources Management System,HRMS),是指组织或社会团体运用系统学理论方法,对企业的人力资源管理方方面面进行分析.规划.实施.调整,提高企业 ...
- CentOS查看显卡及GPU相关信息
lspci | grep -i vga 这样就可以显示机器上的显卡信息,比如 [root@localhost conf]# lspci | grep -i vga01:00.0 VGA compat ...
- 转载 关于restTemplate 内部实现
2016-12-28 by 安静的下雪天 http://www.cnblogs.com/quiet-snowy-day/p/6228198.html 本篇概要 RestTemplate 类图 po ...
- (1/24) 认识webpack
1.什么是webpack (1)webpack是一个模块打包工具,它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript ...
- 什么是“堆”,"栈","堆栈","队列",它们的区别?
堆:什么是堆?又该怎么理解呢? ①堆通常是一个可以被看做一棵树的数组对象.堆总是满足下列性质: ·堆中某个节点的值总是不大于或不小于其父节点的值: ·堆总是一棵完全二叉树. 将根节点最大的堆叫做最大堆 ...