Skip to content

Commit 45adfc7

Browse files
Add custom ListenConfig support for server (#280)
Co-authored-by: SystemGlitch <jeremy.la@outlook.fr>
1 parent dbd9a69 commit 45adfc7

File tree

2 files changed

+98
-2
lines changed

2 files changed

+98
-2
lines changed

server.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ type Options struct {
5151
// HTTP2 configures HTTP/2 connections.
5252
HTTP2 *http.HTTP2Config
5353

54+
// ListenConfig optionally specifies the configuration for the network listener.
55+
// If not provided, the default net.ListenConfig is used.
56+
// This can be useful for customizing keep-alives and other network-level settings
57+
// for optimal performance in large traffic scenarios.
58+
ListenConfig *net.ListenConfig
59+
5460
// ConnState specifies an optional callback function that is
5561
// called when a client connection changes state. See the
5662
// `http.ConnState` type and associated constants for details.
@@ -110,6 +116,7 @@ type Server struct {
110116

111117
ctx context.Context
112118
baseContext func(net.Listener) context.Context
119+
listenConfig *net.ListenConfig
113120
startupHooks []func(*Server)
114121
shutdownHooks []func(*Server)
115122

@@ -163,6 +170,7 @@ func New(opts Options) (*Server, error) {
163170
},
164171
ctx: context.Background(),
165172
baseContext: opts.BaseContext,
173+
listenConfig: opts.ListenConfig,
166174
config: cfg,
167175
services: make(map[string]Service),
168176
Lang: languages,
@@ -267,7 +275,7 @@ func (s *Server) LookupService(name string) (Service, bool) {
267275
return service, ok
268276
}
269277

270-
// RegisterService on thise server using its name (returned by `Service.Name()`).
278+
// RegisterService on this server using its name (returned by `Service.Name()`).
271279
// A service's name should be unique.
272280
// `Service.Init(server)` is called on the given service upon registration.
273281
func (s *Server) RegisterService(service Service) {
@@ -424,7 +432,13 @@ func (s *Server) Start() error {
424432
close(s.stopChannel)
425433
}()
426434

427-
ln, err := net.Listen("tcp", s.server.Addr)
435+
var ln net.Listener
436+
var err error
437+
if s.listenConfig != nil {
438+
ln, err = s.listenConfig.Listen(context.Background(), "tcp", s.server.Addr)
439+
} else {
440+
ln, err = net.Listen("tcp", s.server.Addr)
441+
}
428442
if err != nil {
429443
return errors.New(err)
430444
}

server_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,16 @@ func TestServer(t *testing.T) {
5757
})
5858

5959
http2Cfg := &http.HTTP2Config{}
60+
customListenConfig := &net.ListenConfig{
61+
KeepAlive: 1 * time.Minute,
62+
}
6063
s, err := New(Options{
6164
MaxHeaderBytes: 123,
6265
ConnState: func(_ net.Conn, _ http.ConnState) {},
6366
BaseContext: func(_ net.Listener) context.Context { return context.Background() },
6467
ConnContext: func(ctx context.Context, _ net.Conn) context.Context { return ctx },
6568
HTTP2: http2Cfg,
69+
ListenConfig: customListenConfig,
6670
})
6771
require.NoError(t, err)
6872

@@ -85,6 +89,7 @@ func TestServer(t *testing.T) {
8589
assert.NotNil(t, s.baseContext)
8690
assert.NotNil(t, s.server.BaseContext)
8791
assert.Same(t, http2Cfg, s.server.HTTP2)
92+
assert.Same(t, customListenConfig, s.listenConfig)
8893
assert.Equal(t, "http://127.0.0.1:8080", s.BaseURL())
8994
assert.Equal(t, "http://127.0.0.1:8080", s.ProxyBaseURL())
9095
assert.NoError(t, s.CloseDB())
@@ -696,6 +701,83 @@ func TestServer(t *testing.T) {
696701
assert.Equal(t, "cannot start the server, context is canceled", err.Error())
697702
}
698703
})
704+
705+
t.Run("StartWithCustomListenConfig", func(t *testing.T) {
706+
cfg := config.LoadDefault()
707+
cfg.Set("server.port", 0)
708+
709+
customListenConfig := &net.ListenConfig{
710+
KeepAlive: 1 * time.Minute,
711+
}
712+
713+
server, err := New(Options{
714+
Config: cfg,
715+
ListenConfig: customListenConfig,
716+
})
717+
require.NoError(t, err)
718+
719+
wg := sync.WaitGroup{}
720+
wg.Add(2)
721+
722+
server.RegisterStartupHook(func(s *Server) {
723+
assert.True(t, server.IsReady())
724+
assert.NotEqual(t, 0, s.Port())
725+
726+
res, err := http.Get(s.BaseURL())
727+
defer func() {
728+
assert.NoError(t, res.Body.Close())
729+
}()
730+
assert.NoError(t, err)
731+
respBody, err := io.ReadAll(res.Body)
732+
assert.NoError(t, err)
733+
assert.Equal(t, []byte("hello world"), respBody)
734+
735+
server.Stop()
736+
wg.Done()
737+
})
738+
739+
server.RegisterRoutes(func(_ *Server, router *Router) {
740+
router.Get("/", func(r *Response, _ *Request) {
741+
r.String(http.StatusOK, "hello world")
742+
}).Name("base")
743+
})
744+
745+
go func() {
746+
err := server.Start()
747+
assert.NoError(t, err)
748+
wg.Done()
749+
}()
750+
751+
wg.Wait()
752+
assert.False(t, server.IsReady())
753+
assert.Equal(t, uint32(3), server.state.Load())
754+
})
755+
756+
t.Run("StartWithCustomListenConfigControlError", func(t *testing.T) {
757+
cfg := config.LoadDefault()
758+
cfg.Set("server.port", 0)
759+
760+
// Create a custom ListenConfig with a Control function that always returns an error
761+
// to ensure that the custom config is correctly used if provided.
762+
// The StartWithCustomListenConfig test only checks that the server still works and is able
763+
// to process requests with the custom config.
764+
expectedErr := fmt.Errorf("test control error")
765+
customListenConfig := &net.ListenConfig{
766+
Control: func(_, _ string, _ syscall.RawConn) error {
767+
return expectedErr
768+
},
769+
}
770+
771+
server, err := New(Options{
772+
Config: cfg,
773+
ListenConfig: customListenConfig,
774+
})
775+
require.NoError(t, err)
776+
777+
// Attempt to start the server - it should fail with the error from Control
778+
err = server.Start()
779+
assert.ErrorIs(t, err, expectedErr)
780+
})
699781
}
700782

701783
func TestNoServerFromContext(t *testing.T) {

0 commit comments

Comments
 (0)