11 月 19 - 20 日 Apache Pulsar 社区年度盛会来啦,立即报名! 了解详情
写点什么

Golang 标准库探秘(二):快速搭建 HTTP 服务器

  • 2016-03-06
  • 本文字数:9835 字

    阅读完需:约 32 分钟

服务器阐述:

现在市面上有很多高并发服务器,Nginx 就是一个领军人物,也是我们仰望的存在;Nginx+Lua 这种组合也是高并发服务器的一个代表;PHP 语言作为 Nginx+FastCGI 上一个优秀的解释语言也占据大半江山。而如今的 Golang 也作为高并发服务器语言横空出世,因其“语法简单、代码结构简明,维护成本还低,天生高并发”等特性而被广泛应用,尤其是各种云服务,还有 Docker 也是用 Golang 来做实现语言。

接着我们介绍下服务器编程模型,只从线程的角度,不谈并发模型。

从线程的角度,可以分为“单线程”,“多线程”2 种。

单线程:

整个进程只有一个线程,因为只有一个线程的缘故,当请求来的时候只能一个个按照顺序处理,要想实现高性能只能用“non-blocking IO + IO multiplexing”组合 (非阻塞 io + io 复用)。 Nginx 采用的就是多进程 + 单线程 ( 非阻塞 io+io 复用) 模式。

多线程:

进程有多个线程,多个线程就不好控制,还带来一些问题:锁竞争,数据污染、山下文切换带来的开销,但是可以充分利用 CPU。要实现高性能也是“non-blocking IO + IO multiplexing”组合。

所以,其实不管单线程还是多线程都是要用“non-blocking IO + IO multiplexing”组合的。还有一种用户级线程,整个线程的库都是自己维护,“创建,撤销,切换”,内核是不知道用户级线程存在的,缺点是阻塞时会阻塞整个进程。

其实想实现高并发服务器最好用单线程 (不处理逻辑的情况下),节省很多上下文切换开销 (CPU 分配时间片给任务,CPU 加载上下文),但一定要采用 io 上“非阻塞和异步”。因为多线程很难控制,锁,数据依赖,不同场景会让多线程变成串行,控制起来相当繁琐,牺牲很多并发性能 (Golang 采用的抢占式调度),但正常情况下多线程还是挺不错的。下面我们说下 Golang 实现的高并发。

在 Golang 的调度器里用的也是“csp”并发模型,有 3 个重要的概念 P、M、G。

P 是 Processor,G 是 Goroutine,M 是 Machine。

简述:M 是执行 G 的机器线程,跟 P 绑定才可以执行,P 存放 G 的队列。看到这里大家会问到刚刚不是说多线程切换上下文开销很大吗?其实每个 M 都有一个 g0 栈内存,用来执行运行时管理命令。调度时候 M 的 g0 会从每个 G 栈取出栈 (现场),调度之后再保存到 G,这样不同的 M 就可以接着调度了。所有上下文都是自己在切换,省去了内核带来的开销,而且 Golang 会观察,长时间不调度的 G 会被其他 G 抢占 (抢占调度其实就是一个标记)。

采用异步的方式运行 G,这样就实现了并发 (M 可不止一个啊,感兴趣看下 Go 并发实战

看到上面估计大家可能稍微了解点 Golang 的优势了吧。不要担心 GC 问题,选择场景问题。

实战

现在我们进入实战部分,手把手教你实现 CGI,FastCGI,HTTP 服务器,主要是用 Golang 的 HTTP 包。TCP 实战就不在这次说了,TCP 其实是块难啃的骨头,简单的几乎话说不清楚,如果是简单写一个“hello world”的例子,让大家似懂非懂的,不如单独开篇讲解一下,从 Tcp 到 Protobuf 再到 RPC,然后写一个稍微复杂点的 tcp 服务器,我们也可以处理下“粘包,丢包”等问题 (Protobuf 解决或者做一个分包算法),如果简单的 demo 可能会导致你丢失兴趣的。

首先了解什么是 CGI?CGI 和 FastCGI 的区别是什么?

CGI:全拼 (Common Gateway Interface) 是能让 web 服务器和 CGI 脚本共同处理客户的请求的协议。Web 服务器把请求转成 CGI 脚本,CGI 脚本执行回复 Web 服务器,Web 服务回复给客户端。

CGI fork 一个新的进程来执行,读取参数,处理数据,然后就结束生命期。

FastCGI 采用 tcp 链接,不用 fork 新的进程,因为程序启动的时候就已经开启了,等待数据的到来,处理数据。

看出来差距在哪里了吧?就是 CGI 每次都要 fork 进程,这个开销很大的。(感兴趣的看下 linux 进程相关知识)。

现在我们来做我们的 CGI 服务器

CGI 服务器

需要用到的包:

复制代码
"net/http/cgi"
"net/http"

简单的 2 个包就可以实现 CGI 服务器了。“高秀敏:准备好了吗?希望别看到老头子他又错了的场景啊”。我们按照“代码 -> 讲解”的流程,先运行在讲解。

复制代码
package main;
import (
"net/http/cgi"
"fmt"
"net/http"
)
funcmain() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){
handler := new(cgi.Handler);
handler.Path = "/usr/local/go/bin/go";
script := "/Users/liujinlong/cgi-script" + r.URL.Path;
fmt.Println(handler.Path);
handler.Dir = "/Users/liujinlong/cgi-script";
args := []string{"run", script};
handler.Args = append(handler.Args, args...);
fmt.Println(handler.Args);
handler.ServeHTTP(w, r);
});
http.ListenAndServe(":8989",nil);
select {}// 阻塞进程
}
test.go
package main
import(
"fmt"
)
funcinit() {
fmt.Print("Content-Type: text/plain;charset=utf-8\n\n");
}
funcmain() {
fmt.Println("hello!!!!")
}

看来我们成功了。来看下 net/http/cgi 的包。

先看 host.go,这里有一个重要的结构 Handler。

复制代码
// Handler runs an executable in a subprocess with a CGI environment.
type Handler struct{
Path string // 执行程序
Root string // 处理 url 的根,为空的时候“/”
Dir string // 目录
Env []string // 环境变量
InheritEnv []string // 集成环境变量
Logger *log.Logger// 日志
Args []string // 参数
PathLocationHandlerhttp.Handler //http 包的 handler 宿主
}
func(h *Handler) ServeHTTP(rwhttp.ResponseWriter, req *http.Request)

它也实现了 ServeHttp,所有请求都会调用这个,这个后面分析 HTTP 源码的时候回详细讲解它是做什么的。Handler 是在子程序中执行 CGI 脚本的。

复制代码
funcRequest() (*http.Request, error)
funcServe(handler http.Handler)

先是将前端 CGI 请求转换成 net 包的 HTTP 请求,然后执行 Handler,然后处理 response。

FastCGI 服务器

接下来是 FastCGI 服务器,

用到的包:

复制代码
"net"
"net/http"
"net/http/fcgi"

上面已经讲过,它是 TCP 的方式实现的,需要借助其他服务器来做转发,这里我们只提供代码,demo 的截图讲解 TCP 的时候在加上。

需要使用 Nginx,我电脑上没有。各位自己测试一下

复制代码
server {
listen 80;
server_name ****;
...
location *... {
include fastcgi.conf;
fastcgi_pass 127.0.0.1:9001;
}
...
}//…是省略,自己去写一个 server。(具体谷歌)
package main
import (
"net"
"net/http"
"net/http/fcgi"
)
type FastCGIstruct{}
func(s *FastCGI) ServeHTTP(resphttp.ResponseWriter, req *http.Request) {
resp.Write([]byte("Hello, fastcgi"))
}
funcmain() {
listener, _ := net.Listen("tcp", "127.0.0.1:8989")
srv := new(FastCGI)
fcgi.Serve(listener, srv)
select {
}
}

HTTP 服务器

接下来就是重点了,我们的 HTTP 服务器,这个大家都不陌生,HTTP 是最常用的方式之一,通用性很强,跨团队协作上也比较受到推荐,排查问题也相对来说简单。

我们接下来以 3 种方式来展现 Golang 的 HTTP 服务器的简洁和强大。

  1. 写一个简单的 HTTP 服务器
  2. 写一个稍微复杂带路由的 HTTP 服务器
  3. 分析源码,然后实现一个自定义 Handler 的服务器

然后我们对照 net/http 包来进行源码分析,加强对 http 包的理解。

1、写一个简单的 HTTP 服务器:

复制代码
package main;
import (
"net/http"
)
funchello(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello"))
}
funcsay(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello"))
}
funcmain() {
http.HandleFunc("/hello", hello);
http.Handle("/handle",http.HandlerFunc(say));
http.ListenAndServe(":8001", nil);
select{};// 阻塞进程
}

是不是很简单,我用 2 种方式演示了这个例子,HandleFunc 和 Handle 方式不同,却都能实现一个路由的监听,其实很简单,但是很多人看到这都会有疑惑,别着急,咱们源码分析的时候你会看到。

2、写一个稍微复杂带路由的 HTTP 服务器:

对着上面的例子想一个问题,我们在开发中会遇到很多问题,比如 handle/res,handle/rsa…等等路由,这两个路由接受的参数都不一样,我们应该怎么写。我先来个图展示下运行结果。

是不是挺惊讶的,404 了,路由没有匹配到。可是我们写 handle 这个路由了。

问题:

  1. 什么原因导致的路由失效
  2. 如何解决这种问题,做一个可以用 Controller 来控制的路由

问题 1:

我们在源码阅读分析的时候会解决。

问题 2:

我们可以设定一个控制器 Handle,它有 2 个 action,我们的执行 handle/res 对应的结果是调用 Handle 的控制器下的 res 方法。这样是不是很酷。

来我们先上代码:

静态目录:

  1. css
  2. js
  3. image

静态目录很好实现,只要一个函数 http.FileServer(),这个函数从文字上看就是文件服务器,他需要传递一个目录,我们常以 http.Dir(“Path”) 来传递。

其他目录大家自己实现下,我们来实现问题 2,一个简单的路由。

我们来看下代码

复制代码
package main;
import (
"net/http"
"strings"
"reflect"
"fmt"
)
funchello(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello"));
}
type Handlers struct{
}
func(h *Handlers) ResAction(w http.ResponseWriter, req *http.Request) {
fmt.Println("res");
w.Write([]byte("res"));
}
funcsay(w http.ResponseWriter, req *http.Request) {
pathInfo := strings.Trim(req.URL.Path, "/");
parts := strings.Split(pathInfo, "/");
varaction = "";
fmt.Println(strings.Join(parts,"|"));
if len(parts) >1 {
action = strings.Title(parts[1]) + "Action";
}
fmt.Println(action);
handle := &Handlers{};
controller := reflect.ValueOf(handle);
method := controller.MethodByName(action);
r := reflect.ValueOf(req);
wr := reflect.ValueOf(w);
method.Call([]reflect.Value{wr, r});
}
funcmain() {
http.HandleFunc("/hello", hello);
http.Handle("/handle/",http.HandlerFunc(say));
http.ListenAndServe(":8081", nil);
select{};// 阻塞进程
}

上面代码就可以实现 handle/res,handle/rsa 等路由监听,把前缀相同的路由业务实现放在一个文件里,这样也可以解耦合,是不是清爽多了。其实我们可以在做的更加灵活些。在文章最后我们放出来一个流程图,按照流程图做你们就能写出一个简单的 mvc 路由框架。接下来看运行之后的结果。

如下图:

(点击放大图像)

3、分析源码,然后实现一个自定义 Handler 的服务器

现在我们利用这个例子来分析下 http 包的源码 (只是服务器相关的,Request 我们此期不讲,简单看看就行。)

其实使用 Golang 做 web 服务器的方式有很多,TCP 也是一种,net 包就可以实现,不过此期我们不讲,因为 HTTP 服务器如果不懂,TCP 会让你更加不明白。

我们从入口开始,首先看 main 方法里的 http.HandleFunc 和 http.Handle 这个绑定路由的方法,上面一直没解释有啥区别。现在我们来看一下。

复制代码
// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
funcHandleFunc(pattern string, handler func(ResponseWriter, *Request))
funcHandle(pattern string, handler Handler)

Handle 和 HandleFunc 都是注册路由,从上面也能看出来这两个函数都是绑定注册路由函数的。如何绑定的呢?我们来看下。

上面 2 个函数通过 DefaultServeMux.handle,DefaultServeMux.handleFunc 把 pattern 和 HandleFunc 绑定到 ServeMux 的 Handle 上。

为什么 DefaultServeMux 会把路由绑定到 ServeMux 上呢?

复制代码
// DefaultServeMux is the default ServeMux used by Serve.
varDefaultServeMux = NewServeMux()

因为 DefaultServeMux 就是 ServeMux 的实例对象。导致我们就把路由和执行方法绑注册好了。不过大家请想下 handle/res 的问题?

从上面的分析我们要知道几个重要的概念。

复制代码
HandlerFunc
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFuncfunc(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func(f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

上面的大概意思是,定义了一个函数适配器 (可以理解成函数指针)HandleFunc,通过 HandlerFunc(f) 来进行适配。其实调用的实体是 f 本身。

复制代码
package main
import "fmt"
type A func(int, int)
func(f A)Serve() {
fmt.Println("serve2")
}
funcserve(int,int) {
fmt.Println("serve1")
}
funcmain() {
a := A(serve)
a(1,2)// 这行输出的结果是 serve1
a.Serve()// 这行输出的结果是 serve2
}

上面结果是 serve1,serve2

Golang 的源码里用了很多 HandleFunc 这个适配器。

接下来我们看第二个,ServeMux 结构,最终我们是绑定它,也是通过它来解析。

复制代码
type ServeMuxstruct{
mu sync.RWMutex// 读写锁
m map[string]muxEntry// 路由 map,pattern->HandleFunc
hosts bool// 是否包含 hosts
}
type muxEntrystruct{
explicit bool// 是否精确匹配,这个在 Golang 实现里是 ture
h Handler // 这个路由表达式对应哪个 handler
pattern string// 路由
}

看到 explicit 的时候是不是就明白为啥 handle/res 不能用 handle 来监听了?原来如此。大致绑定流程大家看明白了吗?如果不理解可以回去再看一遍。

接下来我们来看实现“启动 / 监听 / 触发”服务器的代码。

http.ListenAndServe(":8081", nil);上面这句就是,”:8081”是监听的端口,也是 socket 监听的端口,第二个参数就是我们的 Handler,这里我们写 nil。

复制代码
funcListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

从这个代码看出来,Server 这个结构很重要。我们来看看他是什么。

复制代码
type Server struct {
Addr string // 监听的地址和端口
Handler Handler // 所有请求需要调用的 Handler
ReadTimeouttime.Duration // 读的最大 Timeout 时间
WriteTimeouttime.Duration // 写的最大 Timeout 时间
MaxHeaderBytesint // 请求头的最大长度
TLSConfig *tls.Config // 配置 TLS
... // 结构太长我省略些,感兴趣大家自己看下
}

Server 提供的方法有:

复制代码
func(srv *Server) Serve(l net.Listener) error // 对某个端口进行监听,里面就是调用 for 进行 accept 的处理了
func(srv *Server) ListenAndServe() error // 开启 http server 服务
func(srv *Server) ListenAndServeTLS(certFile, keyFile string) error // 开启 https server 服务

Server 的 ListenAndServe 方法通过 TCP 的方式监听端口,然后调用 Serve 里的实现等待 client 来 accept,然后开启一个协程来处理逻辑 (go c.serve)。

它的格式

func(srv *Server) ListenAndServe() error看到这里我们要了解几个重要的概念。

ResponseWriter:生成 Response 的接口

Handler:处理请求和生成返回的接口

ServeMux:路由,后面会说到 ServeMux 也是一种 Handler

Conn : 网络连接

这几个概念看完之后我们下面要用。

复制代码
type conn struct

这个结构是一个网络间接。我们暂时忽略。

这个 c.serve 里稍微有点复杂,它有关闭这次请求,读取数据的,刷新缓冲区的等实现。这里我们主要关注一个 c.readRequest(),通过 redRequest 可以得到 Response,就是输出给客户端数据的一个回复者。

它里面包含 request。如果要看懂这里的实现就要搞懂三个接口。

复制代码
ResponseWriter, Flusher, Hijacker
// ResponseWriter 的作用是被 Handler 调用来组装返回的 Response
type ResponseWriter interface {
// 这个方法返回 Response 返回的 Header 供读写
Header() Header
// 这个方法写 ResponseBody
Write([]byte) (int, error)
// 这个方法根据 HTTP State Code 来写 ResponseHeader
WriteHeader(int)
}
// Flusher 的作用是被 Handler 调用来将写缓存中的数据推给客户端
type Flusher interface {
// 刷新缓冲区
Flush()
}
// Hijacker 的作用是被 Handler 调用来关闭连接的
type Hijacker interface {
Hijack() (net.Conn, *bufio.ReadWriter, error)
}

而我们这里的 w 也就是 ResponseWriter 了。而调用了下面这句方法,就可以利用它的 Write 方法输出内容给客户端了。

serverHandler{c.server}.ServeHTTP(w, w.req)这句就是触发路由绑定的方法了。要看这个触发器我们还要知道几个接口。

具体我们先看下如何实现这三个接口的,因为后面我们要看触发路由执行逻辑片段。实现这三个接口的结构是 response

复制代码
response
// response 包含了所有 server 端的 HTTP 返回信息
type response struct {
conn *conn // 保存此次 HTTP 连接的信息
req *Request // 对应请求信息
chunking bool // 是否使用 chunk
wroteHeaderbool // header 是否已经执行过写操作
wroteContinuebool // 100 Continue response was written
header Header // 返回的 HTTP 的 Header
written int64 // Body 的字节数
contentLength int64 // Content 长度
status int // HTTP 状态
needSniffbool
// 是否需要使用 sniff。(当没有设置 Content-Type 的时候,开启 sniff 能根据 HTTP body 来确定 Content-Type)
closeAfterReplybool
// 是否保持长链接。如果客户端发送的请求中 connection 有 keep-alive,这个字段就设置为 false。
requestBodyLimitHitbool
// 是否 requestBody 太大了(当 requestBody 太大的时候,response 是会返回 411 状态的,并把连接关闭)
}

在 response 中是可以看到

复制代码
func(w *response) Header() Header
func(w *response) WriteHeader(code int)
func(w *response) Write(data []byte) (n int, err error)
func(w *response) WriteString(data string) (n int, err error)
// either dataB or dataS is non-zero.
func(w *response) write(lenDataint, dataB []byte, dataS string) (n int, err error)
func(w *response) finishRequest()
func(w *response) Flush()
func(w *response) Hijack() (rwcnet.Conn, buf *bufio.ReadWriter, err error)

我简单罗列一些,从上面可以看出,response 实现了这 3 个接口。

接下来我们请求真正的触发者也就是 serverHandle 要触发路由 (hijacked finishRequest 暂且不提)。先看一个接口。

复制代码
Handler
type Handler interface {
ServeHTTP(ResponseWriter, *Request) // 具体的逻辑函数
}

实现了 handler 接口,就意味着往 server 端添加了处理请求的逻辑函数。

serverHandle 调用 ServeHttp 来选择触发的 HandleFunc。这里面会做一个判断,如果你传递了 Handler,就调用你自己的,如果没传递就用 DefaultServeMux 默认的。到这整体流程就结束了。

过程是:

DefaultServeMux.ServeHttp 执行的简单流程.

  1. h, _ := mux.Handler®
  2. h.ServeHTTP(w, r) // 执行 ServeHttp 函数

查找路由,mux.handler 函数里又调用了另外一个函数 mux.handler(r.Host, r.URL.Path)。

还记得我们的 ServeMux 里的 hosts 标记吗?这个函数里会进行判断。

复制代码
// Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}

上面就是匹配查找 pattern 和 handler 的流程了

我们来总结一下。

首先调用 Http.HandleFunc

按顺序做了几件事:

  1. 调用了 DefaultServerMux 的 HandleFunc
  2. 调用了 DefaultServerMux 的 Handle
  3. 往 DefaultServeMux 的 map[string]muxEntry 中增加对应的 handler 和路由规则

别忘记 DefaultServerMux 是 ServeMux 的实例。其实都是围绕 ServeMux,muxEntry2 个结构进行操作绑定。

其次调用 http.ListenAndServe(":12345", nil)

按顺序做了几件事情:

  1. 实例化 Server
  2. 调用 Server 的 ListenAndServe()
  3. 调用 net.Listen(“tcp”, addr) 监听端口,启动 for 循环,等待 accept 请求
  4. 对每个请求实例化一个 Conn,并且开启一个 goroutine 处理请求。
  5. 如:go c.serve()
  6. 读取请求的内容 w, err := c.readRequest(),也就是 response 的取值过程。
  7. 调用 serverHandler 的 ServeHTTP,ServeHTTP 里会判断 Server 的属性里的 header 是否为空,如果没有设置 handler,handler 就设置为 DefaultServeMux,反之用自己的 (我们后面会做一个利用自己的 Handler 写服务器)
  8. 调用 DefaultServeMux 的 ServeHttp( 因为我们没有自己的 Handler,所以走默认的)
  9. 通过 request 选择匹配的 handler: A request 匹配 handler 的方式。Hosts+pattern 或 pattern 或 notFound

B 如果有路由满足,返回这个 handler

C 如果没有路由满足,返回 NotFoundHandler
10. 根据返回的 handler 进入到这个 handler 的 ServeHTTP

大概流程就是这个样子,其实在 net.Listen(“tcp”, addr) 里也做了很多事,我们下期说道 TCP 服务器的时候回顾一下他做了哪些。

通过上面的解释大致明白了我们绑定触发的都是 DefaultServeMux 的 Handler。现在我们来实现一个自己的 Handler,这也是做框架的第一步。我们先来敲代码。

复制代码
package main;
import (
"fmt"
"net/http"
"time"
)
type customHandlerstruct{
}
func(cb *customHandler) ServeHTTP( w http.ResponseWriter, r *http.Request ) {
fmt.Println("customHandler!!");
w.Write([]byte("customHandler!!"));
}
funcmain() {
varserver *http.Server = &http.Server{
Addr: ":8080",
Handler: &customHandler{},
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 <<20,
}
server.ListenAndServe();
select {
}
}

是不是很酷,我们可以利用自己的 handler 做一个智能的路由出来。

不过还是建议使用国内 Golang 语言框架 beego ,已开源。一款非常不错的框架,谢大维护的很用心,绝对良心框架,而且文档支持,社区也很不错。

最后附上一张最早设计框架时候的一个流程图 (3 年前)。大家可以简单看看,当然也可以尝试的动动手。起码收获很多。

(点击放大图像)

[1]: http://item.jd.com/11573034.html

[2]: https://github.com/astaxie/beego

作者简介

刘金龙,艺名:金灶沐 ,go 语言爱好者,2015 年 8 月加入创业团队,负责各种“打杂”工作,之前在 360 电商购物小蜜 java 组担任 java 高级工程师职位,负责购物小蜜服务开发。14 年开始用 go 语言做高并发服务并且尝试阅读 go 语言的源码来学习 go 语言的特性。

2016-03-06 16:2727697

评论

发布
暂无评论
发现更多内容

Spring Boot 集成 Redis 配置 MyBatis 二级缓存

微枫Micromaple

redis 缓存 mybatis springboot 10月月更

【LeetCode】重新格式化电话号码Java题解

Albert

LeetCode 10月月更

今日国庆,祝福祖国!【文末超级福利】

图灵教育

读书 国庆节

数组操作の旋转二维数组

掘金安东尼

算法 10月月更

16个分论坛出品标准大揭秘,期待你的申请!

开源社

用任意类型编写代码——一文带你了解泛型编程

宇宙之一粟

Java 泛型编程 10月月更

【从0到1学算法】1. 如何获取题中关键信息

Geek_65222d

10月月更

COSCon'22主论坛来袭 开源站在十字路口

开源社

面试官:高并发场景下,你们是怎么保证数据的一致性的?

一灯架构

Java MySQL 10月月更

微服务稳定性保障

穿过生命散发芬芳

微服务 10月月更

【一Go到底】第一天---初识Goooooooooooooooooooooooo

指剑

Go go并发 10月月更

2022-10-01:给定一个字符串 s,计算 s 的 不同非空子序列 的个数 因为结果可能很大,所以返回答案需要对 10^9 + 7 取余 。 字符串的 子序列 是经由原字符串删除一些(也可能不删除

福大大架构师每日一题

算法 rust 福大大

一起玩OptaPlanner-Study,玩转第一个程序

OptaPlanner中文

《零代码教练指南》正式发布

明道云

[架构实战] 学习笔记二

爱学习的麦子

开发者有话说|程序猿工作多年之后的感悟

慕枫技术笔记

个人成长

存储优化--分区与冷热分离

喵叔

10月月更

Python应用之计算三角形面积

向阳逐梦

10月月更 Python代码 计算三角形面积

大数据ELK(十):使用VSCode操作猎聘网职位搜索案例

Lansonli

ELK 10月月更

OpenHarmony如何控制屏幕亮度

坚果

OpenHarmony 10月月更

今日国庆,祝福祖国!【文末超级福利】

图灵社区

读书 国庆节

ESP32-C3 学习测试 蓝牙 篇(四、GATT Server 示例解析)

矜辰所致

蓝牙 ESP32-C3 10月月更 GATT

MyBatis学习笔记之JDBC

fly

mybatis JDBC 10月月更

简述构建微服务架构的四大挑战

穿过生命散发芬芳

微服务 10月月更

跟随一组图片,了解Go Channel的底层实现

董哥的黑板报

Go 后端 服务端 操作系统 runtime

架构师的十八般武艺:线上运维

agnostic

运维

Collections-Arraylist源码解读(一)

知识浅谈

ArrayList 10月月更

币安链智能合约DAPP模式系统开发搭建

l8l259l3365

体验 Orbeon form PE 版本提供的 JavaScript Embedding API

Jerry Wang

Java SAP commerce 10月月更 oberon

戏说系统安全(50/100)

hackstoic

系统安全

ESP32-C3 学习测试 蓝牙 篇(五、添加 characteristic)

矜辰所致

蓝牙 ESP32-C3 10月月更

Golang标准库探秘(二):快速搭建HTTP服务器_语言 & 开发_刘金龙_InfoQ精选文章