NIO TCP Chat Server

Hey Guys, I am gathering information on proper practices in building a TCP Chat Server. In essence the Chat Server should be able to open chatrooms and Direct Messages to individuals. Then send the messages and chatroom information to the Rest API in order to save it to the database. Currently I am able to do the Server Functionality, but I am not able to create rooms or broadcast messages only between 2 users. What would be a better approach to accomplish this functionality? Is IRC something I want to look into or can I just do this in SwiftNIO. I've done this before in web sockets, where a wss:// url will get a room created or a session between 2 users, but writing it with TCP is something new to me.

My Server Repo is here:
https://github.com/Cartisim/CartisimTCP/tree/main/Sources/CartisimTCP

Thank you for your time

You can absolutely do this with SwiftNIO and regular old TCP. What you will need to do is to add a framing layer: some layer of binary data around your messages that you can use to transmit control data and other information.

The simplest kind of framing is length-prefixed framing: sending a fixed-width length field before your data. You can do this with the LengthFieldBasedFrameDecoder and the LengthFieldPrepender from swift-nio-extras. These ChannelHandlers will add-and-remove the length based framing.

2 Likes

If you haven't seen that, I've build a full IRC chat stack on top of just SwiftNIO, including:

I think the implementation is not too awful and could be used for inspiration. I demo'd the thing at the Swift server conf: video. You can sometimes try it over here: irc.noze.io.

It does not provide persistence, Redi/S could be used to implement that. With its pub/sub support Redis makes it very easy to implement chat clients.

What protocol you use (IRC, XMPP, Matrix, or your custom) really depends on your goal. IRC is a really simple one. One advantage of using a standard protocol is that you get clients and other infrastructure for free ...

Finally you need to consider your scale. If everything fits on one server, things are very easy. Once you need to use multiple instances, things quickly get much more complicated :slight_smile:

3 Likes

Thank you for this guidance, I am using LineBasedFrameDecoder for framing. Is LengthFieldBasedFrameDecoder something I want to add on top of that?

return ServerBootstrap(group: group)
        
        .childChannelInitializer { channel in
            channel.pipeline.addHandlers([
                NIOSSLServerHandler(context: sslContext!),
                BackPressureHandler()
            ])
            .flatMap {
                channel.pipeline.addHandlers([
                    ByteToMessageHandler(LineBasedFrameDecoder()),
                    self.chatHandler,
                    MessageToByteHandler(JSONMessageEncoder<EncryptedObject>())
                ])
            }
        }
        .childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)

My Confusion is, How do I tell NIO to only create a channel for 2 users for the DM feature and specify a channel for chatrooms for the chatroom feature?

Are you happy for the two users to be connected through your server?

I believe so, Is that not a common practice? Like on Discord you have the option to DM people. I would assume that the DM feature runs through their server?

It is, I just wanted to double-check and constrain things.

So, in the first instance I think I would avoid creating new Channels: they add complexity. Instead, I'd add multiplexing to my protocol. A simple way to do that would be to essentially copy IRC.

For example, right now your message format is line-based. That's fine, what you could do is say that your messages are of the form message_target:message_content. Then, when your user wants to DM johnnyappleseed, they send a message over TCP that reads johnnyappleseed:Hey Johnny, how's it going?. This can be repurposed to allow channel names in the first field.

Ah ok, thank you. I believe that makes sense. I will look into the IRC logic and read up on multiplexing.

This is the place in irc-server where it decides whether to send a message to another user (a DM) or to a channel: swift-nio-irc-server/IRCSessionDispatcher.swift at develop · NozeIO/swift-nio-irc-server · GitHub

(essentially the server has to track the connected users (and their NIO Channel) and the available IRC channels with the respective members)

Note that the IRC protocol encoder/decoder is separated out in an own package which just converts the byte level data on the socket to higher level IRC messages. This can be found over here: GitHub - SwiftNIOExtras/swift-nio-irc: A Internet Relay Chat (IRC) protocol implementation for SwiftNIO

I started to took a look. It looks super interesting. Thank you guys for your work.

Hey @Helge_Hess1 thank for NIOIRC! I have been learning your NIOIRCClient a bit. I am having trouble getting it to connect to a server because the channel is never returning. It seems to be async all the way up into the client, so calling wait() seems to only want to crash it. In my code I am not creating the EventLoopGroup in the app, but I am creating it in the Client. However with both of the NIOIRC iOS example and my app are experiencing the same behavior.

I also tried changing the .channelInitializer to .addHandlers([ IRCChannelHandler(), Handler(client: me) ]) But then neither of the channels are actually initialized and are empty.

Any suggestions?

`

private func _connect(host: String, port: Int) -> EventLoopFuture<Channel> {
assert(eventLoop.inEventLoop,    "threading issue")
assert(state.canStartConnection, "cannot start connection!")

clearListCollectors()
userMode = IRCUserMode()
state    = .connecting

retryInfo.attempt += 1

return bootstrap.connect(host: host, port: port)
  .map { channel in
    self.retryInfo.registerSuccessfulConnect()
    
    guard case .connecting = self.state else {
      assertionFailure("called \(#function) but we are not connecting?")
      return channel
    }
    
    self.state = .registering(channel: channel,
                              nick:     self.options.nickname,
                              userInfo: self.options.userInfo)
    self._register()
   /// never returns the channel. NOTE: When we .addHandler(Handler(client: me). this handler seems to never get called or returns without being initialized. Perhaps because IRCChannelHandler() never finishes? 
    return channel
  } 
}

`