基于TCP实现简单的聊天室
原文出处:《Go 语言编程之旅》第四章4.1节
基于TCP的聊天室
1、服务端
- 新用户到来,生成一个User的实例,代表该用户。
type User struct{
ID int // 用户的唯一标识,通过GenUserID 函数生成
Addr string // 用户的IP地址和端口
EnterAt time.Time // 用户进入的时间
MessageChannel chan string // 当前用户发送消息的通道
}
- 新开一个goroutine用于给用户发送消息
func sendMessage(conn net.Conn, ch <- chan string){
for msg := range ch{
fmt.Fprintln(conn, msg)
}
}
结合User结构体的MessageChannel,很容易知道,需要给某个用户发送消息,只需要往该用户的MessageChannel中写入消息即可。这里需要特别提醒下,因为sendMessage在一个新的goroutine中,如果函数的ch不关闭,该goroutine是不会退出的,因此需要注意不关闭ch导致goroutine泄露问题。
- 给当前用户发送欢迎信息,同时给聊天室所有的用户发送有新用户到来的提醒
user.MessageChannel <- "Welcome" + user.String()
msg := Message{
OwnerID: user.ID,
Content: "user:`" + strconv.Itoa(user.ID) + "` has enter",
}
messageChannel <- msg
- 将该新用户写入全局用户列表,也就是聊天室用户列表。同时控制用户超时退出,超过5分钟没有任何响应,则提出
enteringChannel <- user
// 控制超时用户踢出
var userActive = make(chan struct{})
go func() {
d := 5 * time.Minute
timer := time.NewTimer(d)
for{
select {
case <- timer.C:
conn.Close()
case <- userActive:
timer.Reset(d)
}
}
}()
读取用户的输入,并将用户信息发送给其他用户。
在bufio包中有多重方式获取文本输入,ReadBytes、ReadString和独特的ReadLine,对于简单的目的这些都有些复杂。在Go1,1中添加了一个新类型,Scabber,以便更容易的处理如按行读取输入序列或空格分隔单词等这类简单任务。它终结了如输入一个很长的有问题的行这样的输入错误,并且提供了简单的默认行为:基于行的输入,每行都提出了分隔标识。
// 循环读取用户的输入
input := bufio.NewScanner(conn)
for input.Scan(){
msg.Content = strconv.Itoa(user.ID) + ";" + input.Text()
messageChannel <- msg
// 用户活跃
userActive <- struct{}{}
}
if err := input.Err();err != nil {
log.Println("读取错误:", err)
}
- 用户离开,需要做登记,并给连天使其他用户发通知
leavingChannel <- user
msg.Content = "user: `" + strconv.Itoa(user.ID) + "` has left"
messageChannel <- msg
完整代码
package main
import (
"bufio"
"fmt"
"log"
"net"
"strconv"
"sync"
"time"
)
type User struct{
ID int // 用户的唯一标识,通过GenUserID 函数生成
Addr string // 用户的IP地址和端口
EnterAt time.Time // 用户进入的时间
MessageChannel chan string // 当前用户发送消息的通道
}
// 给用户发送信息
type Message struct{
OwnerID int
Content string
}
var (
// 新用户到来,通过该channel进行登记
enteringChannel = make(chan *User)
// 用户离开,通过该channel进行登记
leavingChannel = make(chan *User)
// 广播专用的用户普通消息channel, 缓冲是尽可能避免出现异常情况阻塞
messageChannel = make(chan Message, 9)
)
func (u *User) String() string{
return u.Addr + ",UID:" + strconv.Itoa(u.ID) + ", Enter At:" + u.EnterAt.Format("2006-01-02 15:04:05+8000")
}
func main() {
listener, err := net.Listen("tcp",":2020")
if err != nil {
panic(err)
}
go broadcaster()
for {
conn, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}
go handleConn(conn)
}
}
// broadcaster 用于记录聊天室用户,并进行消息广播
// 1. 新用户进来; 2.用户普通消息; 3.用户离开
func broadcaster(){
users := make(map[*User]struct{})
for {
select{
case user := <- enteringChannel:
// 新用户进入
users[user] = struct{}{}
case user := <- leavingChannel:
// 用户离开
delete(users, user)
// 避免goroutine泄露
close(user.MessageChannel)
case msg := <-messageChannel:
// 给所有在线用户发送消息
for user := range users {
if user.ID == msg.OwnerID{
continue
}
user.MessageChannel <- msg.Content
}
}
}
}
func handleConn(conn net.Conn){
defer conn.Close()
// 1. 新用户进来,构建该用户实例
user := &User{
ID: GenUserID(),
Addr: conn.RemoteAddr().String(),
EnterAt: time.Now(),
MessageChannel: make(chan string,8),
}
// 2. 当前在一个新的goroutine 中,用来进行读写操作,因此需要开一个goroutine用于读写操作
// 读写goroutine 之间通过channel 进行通信
go sendMessage(conn, user.MessageChannel)
// 3. 给当前用户发送欢迎信息;给所有用户告知新用户列表
user.MessageChannel <- "Welcome" + user.String()
msg := Message{
OwnerID: user.ID,
Content: "user:`" + strconv.Itoa(user.ID) + "` has enter",
}
messageChannel <- msg
// 4. 将该记录到全局的用户列表中,避免用锁
enteringChannel <- user
// 控制超时用户踢出
var userActive = make(chan struct{})
go func() {
d := 5 * time.Minute
timer := time.NewTimer(d)
for{
select {
case <- timer.C:
conn.Close()
case <- userActive:
timer.Reset(d)
}
}
}()
// 5. 循环读取用户的输入
input := bufio.NewScanner(conn)
for input.Scan(){
msg.Content = strconv.Itoa(user.ID) + ";" + input.Text()
messageChannel <- msg
// 用户活跃
userActive <- struct{}{}
}
if err := input.Err();err != nil {
log.Println("读取错误:", err)
}
// 6. 用户离开
leavingChannel <- user
msg.Content = "user: `" + strconv.Itoa(user.ID) + "` has left"
messageChannel <- msg
}
func sendMessage(conn net.Conn, ch <- chan string){
for msg := range ch{
fmt.Fprintln(conn, msg)
}
}
// 生成用户id
var (
globalID int
idocker sync.Mutex
)
func GenUserID() int {
idocker.Lock()
defer idocker.Unlock()
globalID ++
return globalID
}
2、客户端
客户端的实现直接采用 《The Go Programming Language》一书对应的示例源码:ch8/netcat3/netcat.go 。
func main() {
conn, err := net.Dial("tcp", ":2020")
if err != nil {
panic(err)
}
done := make(chan struct{})
go func() {
io.Copy(os.Stdout, conn) // NOTE: ignoring errors
log.Println("done")
done <- struct{}{} // signal the main goroutine
}()
mustCopy(conn, os.Stdin)
conn.Close()
<-done
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
- 新开了一个 goroutine 用于接收消息;
- 通过 io.Copy 来操作 IO,包括从标准输入读取数据写入 TCP 连接中,以及从 TCP 连接中读取数据写入标准输出;
- 新开的 goroutine 通过一个 channel 来和 main goroutine 通讯;
基于TCP实现简单的聊天室的更多相关文章
- 基于TCP/IP的局域网聊天室---C语言
具备注册账号,群聊,查看在线人员信息,私发文件和接收文件功能,因为每个客户端只有一个属于自己的socket,所以无论客户端是发聊天消息还是文件都是通过这一个socket发送, 这也意味着服务器收发任何 ...
- 基于websocket实现的一个简单的聊天室
本文是基于websocket写的一个简单的聊天室的例子,可以实现简单的群聊和私聊.是基于websocket的注解方式编写的.(有一个小的缺陷,如果用户名是中文,会乱码,不知如何处理,如有人知道,请告知 ...
- 基于LINUX的多功能聊天室
原文:基于LINUX的多功能聊天室 基于LINUX的多功能聊天室 其实这个项目在我电脑已经躺了多时,最初写完项目规划后,我就认认真真地去实现了它,后来拿着这个项目区参加了面试,同样面试官也拿这个项目来 ...
- Android简单的聊天室开发(client与server沟通)
请尊重他人的劳动成果.转载请注明出处:Android开发之简单的聊天室(client与server进行通信) 1. 预备知识:Tcp/IP协议与Socket TCP/IP 是Transmission ...
- Netty学习笔记(四) 简单的聊天室功能之服务端开发
前面三个章节,我们使用了Netty实现了DISCARD丢弃服务和回复以及自定义编码解码,这篇博客,我们要用Netty实现简单的聊天室功能. Ps: 突然想起来大学里面有个课程实训,给予UDP还是TCP ...
- 基于EPOLL模型的局域网聊天室和Echo服务器
一.EPOLL的优点 在Linux中,select/poll/epoll是I/O多路复用的三种方式,epoll是Linux系统上独有的高效率I/O多路复用方式,区别于select/poll.先说sel ...
- 基于 OpenResty 实现一个 WS 聊天室
基于 OpenResty 实现一个 WS 聊天室 WebSocket WebSocket 协议分析 WebSocket 协议解决了浏览器和服务器之间的全双工通信问题.在WebSocket出现之前,浏览 ...
- 玩转Node.js(四)-搭建简单的聊天室
玩转Node.js(四)-搭建简单的聊天室 Nodejs好久没有跟进了,最近想用它搞一个聊天室,然后便偶遇了socket.io这个东东,说是可以用它来简单的实现实时双向的基于事件的通讯机制.我便看了一 ...
- TCP/IP以及Socket聊天室带类库源码分享
TCP/IP以及Socket聊天室带类库源码分享 最近遇到个设备,需要去和客户的软件做一个网络通信交互,一般的我们的上位机都是作为客户端来和设备通信的,这次要作为服务端来监听客户端,在这个背景下,我查 ...
- 简单的聊天室代码php+swoole
php swoole+websocket 客户端代码 <!DOCTYPE html> <html> <head> <title></title&g ...
随机推荐
- 【前端动画】—— 再看tweenJS
16开始接触前端,一直对一个问题特别感兴趣,那就是js动画,也就是从那时起开始探究动画的各种表现形式,也是那个时候开始意识到编程这块东西最终考验的就是抽象和逻辑,而这一切完全是数学里边的东西. 最早接 ...
- [I.1] 个人作业:阅读和提问
个人作业:阅读和提问 项目 内容 这个作业属于哪个课程 2025年春季软件工程(罗杰.任健) 这个作业的要求在哪里 [I.1] 个人作业:阅读和提问 我在这个课程的目标是 学习并掌握软件工程方法,与团 ...
- docker login harbor http login登录
前言 搭建的 harbor 仓库为 http 协议,在本地登录时出现如下报错: docker login http://192.168.xx.xx Username: admin Password: ...
- Django实战项目-学习任务系统-用户登录
第一步:先创建一个Django应用程序框架代码 1,先创建一个Django项目 django-admin startproject mysite 将创建一个目录,其布局如下: mysite/ mana ...
- SpringBoot原理分析-1
SpringBoot原理分析 作为一个javaer,和boot打交道是很常见的吧.熟悉boot的人都会知道,启动一个springboot应用,就是用鼠标点一下启动main方法,然后等着就行了.我们来看 ...
- python实现排列组合--itertools
这是一个python自带的工具集,简单好用功能强大,能够大大提升编写代码效率. 功能不止排列组合,其他的用用加深理解了再整理. 官方文档:https://docs.python.org/zh-cn/3 ...
- 什么是单点登录?什么是SSO?什么是CAS?
目录 单点登录简介 SSO&CAS是什么 单点登录适合什么场景 单点登录的三种实现方式 CAS的几个重要知识点 CAS的实现过程 单点登录简介 单点登录(SingleSignOn,SSO),就 ...
- 面试题-Netty框架
前言 Netty框架部分的题目,是我根据Java Guide的面试突击版本V3.0再整理出来的,其中,我选择了一些比较重要的问题,并重新做出相应回答,并添加了一些比较重要的问题,希望对大家起到一定的帮 ...
- golang实现命令行程序的使用帮助
通过flag包我们可以很方便的实现命令行程序的参数标志, 接下来我们来看看如何实现命令行程序的使用帮助, 通常以参数标志-h或--help的形式来使用. 自动生成使用帮助 我们只需要声明其他参数标志, ...
- Spring解决创建单例bean,而存在线程不安全问题,的解决方案
一.线程安全问题都是由全局变量.静态变量和类的成员变量引起的.若每个线程中对全局变量.静态变量和类的成员变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的,反之线程存在问题 二.因为Sp ...