golang 一个简单的命令行聊天室,如irc

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang 一个简单的命令行聊天室,如irc相关的知识,希望对你有一定的参考价值。

package tools

import (
	"bufio"
	"os"
	"strings"
)

func SanLine() string {
	inputReader := bufio.NewReader(os.Stdin)
	input,_ := inputReader.ReadString('\n')
	return strings.Replace(input, "\n", "", -1)
}
package main

import (
	"os"
	"fmt"
	"net"
	"log"
	"strings"
	"goLearn/chatroom/ircTest/tools"
)

func main() {
	// conns[string]net.Conn用于保存昵称和conn对应关系的map
	conns := make(map[string]net.Conn)
	//传递消息的channel
	msgCh := make(chan string, 10)

	if len(os.Args) != 2 {
		fmt.Println("PLease input: \" cmd [:port] \"")
		os.Exit(0)
	}
	port := os.Args[1]
	addr, err := net.ResolveTCPAddr("tcp", port)
	if err != nil {
		log.Fatal(err)
	}
	// 开始监听
	listener, err := net.ListenTCP("tcp", addr)
	log.Println("服务器启动")

	// 启动广播协程
	go broadcast(msgCh, &conns)
	// 启动serverTools()协程
	go serverTools(msgCh, &conns)
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Fatal(err) //todo:这里可能有问题
		}
		go ServerHandle(conn, msgCh, &conns)
	}
}

func ServerHandle(conn net.Conn, msgCh chan string, conns *map[string]net.Conn) {
	//发送欢迎信息
	conn.Write([]byte("您已连接"))
	var msg string
	data := make([]byte, 1024)

	for {
		length, err := conn.Read(data)
		// 如果读取错误,可以认为是客户端退出或者关闭了连接,
		// 那么就关闭与客户端的连接,break出for-loop,相当于退出了这个conn的handle()函数
		if err != nil {
			// 这里有问题,如果客户端没有quit,而是直接退出,那么这个昵称及conn的键值对还在conns里,
			// 下次连接时,服务器查询conns,会认为这个昵称已存在
			conn.Close()
			break
		}
		// 如果读到消息,那么将消息有效部分之后的一位置为0
		// todo: 暂时不知道为什么这么做
		if length > 0 {
			data[length] = 0
		}

		// 客户端发来的消息是带"前缀"的,解析消息,将"前缀"和消息体分开
		cmd := strings.Split(string(data[:length]), "|")
		// 打印带有前缀的消息有效部分
		fmt.Println(string(data[:length]))

		// 开始判断消息前缀,做相应的处理
		switch cmd[0] {
		// 前缀"hello"代表客户端请求一个昵称
		case "hello":
			// 昵称在conns中已经存在,代表重名,退出客户端连接
			// todo:这里怎么样可以改成不断开,而是让客户端重新尝试起名字
			if _, ok := (*conns)[cmd[1]]; ok {
				conn.Close()
			} else {
				// 将昵称和conn的键值对存入map,昵称作为key
				(*conns)[cmd[1]] = conn
				msg = fmt.Sprintf("[%s] join...", cmd[1])
			}
		case "say":
			msg = fmt.Sprintf("[%s] : %s", cmd[1], cmd[2])
		case "quit":
			msg = fmt.Sprintf("quit|[%s]", cmd[1])
		}

		// 将匹配模式生成的消息传给channel
		msgCh <- msg
	}
}

func broadcast(msgCh chan string, conns *map[string]net.Conn) {
	for {
		msg := <-msgCh
		// 遍历所有客户端,向客户端发送消息
		for name, conn := range *conns {
			_, err := conn.Write([]byte(msg))
			// 如果向某一个客户端发送消息失败,那么我们认为该客户端已经退出或者关闭了连接,
			// 那么就需要将该客户端在conns中保存的键值对删掉
			if err != nil {
				delete(*conns, name)
			}
		}
	}
}

// serverTools() 用来定义server端的一些行为和工具,例如server的发言,server踢人等。
func serverTools(msgCh chan string, conns *map[string]net.Conn)  {
	msg := tools.SanLine()
	// cmd是一个[]string
	cmd := strings.Split(string(msg), "|")

	log.Println(cmd)

	if len(cmd) > 1{
		switch cmd[0] {
		case "kick":
			if _, ok := (*conns)[cmd[1]]; ok{
				(*conns)[cmd[1]].Close()
				msgCh <- "[Server]: Kick out [" + cmd[1] + "]"
			}
		default:
			msgCh <- "[Server]" + string(msg)
		}
	}else{
		msgCh <- "[Server]:" + string(msg)
	}

}


package main

import (
	"os"
	"fmt"
	"net"
	"log"
	"strings"
	"goLearn/chatroom/ircTest/tools"
)

func main() {
	// conns[string]net.Conn用于保存昵称和conn对应关系的map
	conns := make(map[string]*net.Conn)
	//传递消息的channel
	msgCh := make(chan string, 10)

	if len(os.Args) != 2 {
		fmt.Println("PLease input: \" cmd [:port] \"")
		os.Exit(0)
	}
	port := os.Args[1]
	addr, err := net.ResolveTCPAddr("tcp", port)
	if err != nil {
		log.Fatal(err)
	}
	// 开始监听
	listener, err := net.ListenTCP("tcp", addr)
	log.Println("服务器启动")

	// 启动广播协程
	go broadcast(msgCh, conns)
	// 启动serverTools()协程
	go serverTools(msgCh, conns)
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Fatal(err)
		}
		go ServerHandle(&conn, msgCh, conns)
	}
}

func ServerHandle(conn *net.Conn, msgCh chan string, conns map[string]*net.Conn) {

	//发送欢迎信息
	(*conn).Write([]byte("您已连接"))
	var msg string
	data := make([]byte, 1024)

	for {
		length, err := (*conn).Read(data)
		// 如果读取错误,可以认为是客户端退出或者关闭了连接,
		// 那么就关闭与客户端的连接,break出for-loop,相当于退出了这个conn的handle()函数
		if err != nil {
			(*conn).Close()
			// 如果客户端没有通过conn.Close()来断开连接,而是直接关闭程序或者网络中断,那么服务器端主动断开链接,
			// 并且删除保存在conns中的项目,以避免下次客户端登陆时,出现因为同名而断开的情况。
			for name, connItem := range conns {
				if conn == connItem {
					delete(conns, name)
				}
			}
			break
		}

		// 客户端发来的消息是带"前缀"的,解析消息,将"前缀"和消息体分开
		cmd := strings.Split(string(data[:length]), "|")
		// 打印带有前缀的消息有效部分
		fmt.Println(string(data[:length]))

		// 开始判断消息前缀,做相应的处理
		switch cmd[0] {
		// 前缀"hello"代表客户端请求一个昵称
		case "hello":
			// 昵称在conns中已经存在,代表重名,退出客户端连接
			// todo:这里怎么样可以改成不断开,而是让客户端重新尝试起名字
			if _, ok := conns[cmd[1]]; ok {
				(*conn).Write([]byte("重名啦!!!!!"))
				(*conn).Close()
			} else {

				// 将昵称和conn的键值对存入map,昵称作为key
				/*
				指针的定以后如果没有指向任何变量,那么为nil(空指针)。*p = *q这种表达式,是把*p指向的内存地址报错
				的数据更改为*q指向内存地址保存的数据,相当于复制了一份,内存地址还是不一样。操作的是数据。所以下面这
				里表达式两遍的指针不能加*,下面代码的目的是为了把conn的内存地址保存在conns中,以便于后续代码操作的
				是同一个conn。如果加了*(前提是conns[cmd[1]]已经被初始化过),那么相当于是把conn指向的值赋值给了
				conns[1]指针指向的内存,两个指针指向的内存还是不一样。
				*/
				conns[cmd[1]] = conn
				msg = fmt.Sprintf("[%s] join...", cmd[1])
			}
		case "say":
			msg = fmt.Sprintf("[%s] : %s", cmd[1], cmd[2])
		case "quit":
			msg = fmt.Sprintf("quit|[%s]", cmd[1])
		}

		// 将匹配模式生成的消息传给channel
		msgCh <- msg
	}
}

func broadcast(msgCh chan string, conns map[string]*net.Conn) {
	for {
		msg := <-msgCh
		// 遍历所有客户端,向客户端发送消息
		for name, conn := range conns {
			_, err := (*conn).Write([]byte(msg))
			// 如果向某一个客户端发送消息失败,那么我们认为该客户端已经退出或者关闭了连接,
			// 那么就需要将该客户端在conns中保存的键值对删掉
			if err != nil {
				delete(conns, name)
			}
		}
	}
}

// serverTools() 用来定义server端的一些行为和工具,例如server的发言,server踢人等。
func serverTools(msgCh chan string, conns map[string]*net.Conn) {
	for {
		msg := tools.SanLine()
		// cmd是一个[]string
		cmd := strings.Split(string(msg), " ")
		var kickOutInfo string
		if len(cmd) > 1 {
			switch cmd[0] {
			case "kick":
				log.Println("check kick")
				var conn *net.Conn
				conn = conns[cmd[1]]
				if _, ok := conns[cmd[1]]; ok {
					(*conn).Close()
					delete(conns, cmd[1])
					log.Println(conns)
					//msgCh <- "[Server]: Kick out [" + cmd[1] + "]"
					kickOutInfo = "[Server]: Kick out [" + cmd[1] + "]"
				}
			default:
				if len(kickOutInfo) > 0 {
					msgCh <- kickOutInfo
				} else {
					msgCh <- "[Server]" + string(msg)
				}
			}
		} else {
			msgCh <- "[Server_say]:" + string(msg)
		}
	}
}
package main

import (
	"os"
	"fmt"
	"net"
	"log"
	"strings"
	"goLearn/chatroom/ircTest/tools"
)

func main() {
	if len(os.Args) != 2 {
		fmt.Println("PLease input: \" cmd [:port | IPaddress:port] \"")
		os.Exit(0)
	}
	port := os.Args[1]
	addr, err := net.ResolveTCPAddr("tcp", port)
	if err != nil {
		log.Fatal(err)
	}
	conn, err := net.DialTCP("tcp", nil, addr)
	if err != nil {
		log.Fatal(err)
	}

	// 用于保存消息
	msg := make([]byte, 1024)
	// 读取欢迎消息
	conn.Read(msg)
	fmt.Println(string(msg))

	// 输入昵称
	fmt.Print("请输入昵称:")
	nickname := tools.SanLine()
	fmt.Println("Hello", nickname)

	// 向服务器发送"hello|nickname",尝试定义昵称,如果昵称重名,这里会直接退出,尝试修改为可以不断尝试
	conn.Write([]byte("hello|" + nickname))

	// 开启接收数据的协程
	go ClientHandle(conn, nickname)

	// 发送消息
	// 考虑一下放在一个单独的函数中怎么实现
	for{
		msg := tools.SanLine()
		if msg == "quit"{
			conn.Write([]byte("quit|" + nickname))
			// 如果发送quit,客户端主动关闭conn
			conn.Close()
		}
		conn.Write([]byte("say|" + nickname + "|" + msg))
	}
}


func ClientHandle(conn *net.TCPConn, nickname string) {
	for{
		msg := make([]byte, 1024)
		_, err := (*conn).Read(msg)
		if err != nil {
			// 如果这里从服务器端conn读取失败,可以认为是服务器关闭连接或者退出了
			log.Fatal(err)
		}
		// 将自身的发言屏蔽
		if strings.Contains(string(msg), "["+nickname+"] : ") == false{
			fmt.Println(string(msg))
		}
	}
}

package main

import (
	"os"
	"fmt"
	"net"
	"log"
	"strings"
	"goLearn/chatroom/ircTest/tools"
)

func main() {
	if len(os.Args) != 2 {
		fmt.Println("PLease input: \" cmd [:port | IPaddress:port] \"")
		os.Exit(0)
	}
	port := os.Args[1]
	addr, err := net.ResolveTCPAddr("tcp", port)
	if err != nil {
		log.Fatal(err)
	}
	conn, err := net.DialTCP("tcp", nil, addr)
	if err != nil {
		log.Fatal(err)
	}

	// 用于保存消息
	msg := make([]byte, 1024)
	// 读取欢迎消息
	conn.Read(msg)
	fmt.Println(string(msg))

	// 输入昵称
	fmt.Print("请输入昵称:")
	nickname := tools.SanLine()
	fmt.Println("Hello", nickname)

	// 向服务器发送"hello|nickname",尝试定义昵称,如果昵称重名,这里会直接退出,尝试修改为可以不断尝试
	conn.Write([]byte("hello|" + nickname))

	// 开启接收数据的协程
	go ClientHandle(conn, nickname)

	// 发送消息
	for {
		msg := tools.SanLine()
		if msg == "quit" {
			conn.Write([]byte("quit|" + nickname))
			// 如果发送quit,客户端主动关闭conn
			conn.Close()
		}
		conn.Write([]byte("say|" + nickname + "|" + msg))
	}
}

func ClientHandle(conn *net.TCPConn, nickname string) {
	for {
		msg := make([]byte, 1024)
		_, err := (*conn).Read(msg)
		if err != nil {
			// 如果这里从服务器端conn读取失败,可以认为是服务器关闭连接或者退出了
			log.Fatal(err)
		}
		// 将自身的发言屏蔽
		if strings.Contains(string(msg), "["+nickname+"] : ") == false {
			fmt.Println(string(msg))
		}
	}
}

irc使用教程

下面介绍几个IRC名词:
NICKNAME (或nick) 昵称。在命令中可以表示你本人或者其他聊天客。
#CHANNEL (或#chan) 频道、聊天室房间名字。房间名字前面一定要加 # 符号。
服务器机器人 是irc上的服务器机器人。他的最基本职责是呆在房间内并使房间继续生效。在cr1.3以后的irc服务器里面,只有注册了的房间才会有守房间的机器人。
帽子 就是管理权限标志@的俗称.取之于乌纱帽.这个@标志出现在名字的前面时,该人士即具有踢人和封人的权力,当然,@可以是临时或者固定的
IP 就是你在互联网上的地址.在这里需要强调的是,这个地址应是保密的,如果一些不法用户知道你的真实IP,就会对你不利.


1.irc 可以直接在网页上聊天
IRC服务器
http://webchat.freenode.net(用户量最大的,频道最多的应该是freenode,大的开源软件一般在上面都有对应的频道。)
https://irc.gitter.im/ 
https://kiwiirc.com/client  
https://users.dal.net/

2.使用客户端

XChat: 典型的linux风格软件(有windows版本),我个人喜欢使用的是XChat;
HexChat:跨平台支持,基于XChat
mIRC: 声称是使用最多的IRC软件, win下很多人使用;
ChatZilla: Mozilla浏览器下的插件IRC客户端, 在windows下我选择了使用该软件, 直接在firefox下扩展CZ插件既可使用.

3.注册及验证身份

进行注册(这个email是一个关键,如果你忘了密码,如果管理员不能确定你是合法使用者时,会把密码发到注册的那个信箱里面。)
/msg [email protected] REGISTER 密码 邮箱
或者
/NickServ REGISTER 密码 邮箱

注册成功后, 会收到相应server所发送的确认邮件, 内容如下, 大致就是说你的user是什么 注册后需要输入确认命令(紫色部分的命令)在服务器来确认你的注册:
/msg NickServ VERIFY REGISTER bluetata waqlxsesxqou

验证身份
/msg NickServ IDENTIFY 昵称 密码

修改昵称用户名
/nick 新昵称

迁移权限: 如果你通过注册并且认证了某个昵称, 后更改了新昵称, 并且想要拥有之前昵称的权限, 需要使用如下
/msg nickserv group 新昵称 密码

注意: 虽然是注册了, 但是,如果你3个月, 都没有进IRC聊天, 那么这个昵称, 就会被服务器注销, 需要重新验证身份.

4.用户密码

1.忘记密码

如果太长时间没登录IRC,难免会忘记密码,那IRC有重置密码的功能吗?
当然有,不过也是通过命令行进行操作的,相当geek:)。
此功能是服务器通过提供NickServ服务(其实语法上就是一个用户,
类似的服务还有ChanServ MemoServ)实现的。

假定需要重置密码的用户名为foo,那首先可以查看下账户信息,可以看到注册时间,最后一次登录时间及IP:
/msg NickServ INFO foo

接下来,通过以下命令找回密码,服务器会往注册邮箱发送一封包含临时字串的邮件:
/msg NickServ SENDPASS foo

根据临时密码字串,就可以重新设置密码了:
/msg NickServ SETPASS temp_string mynewpass

2.修改密码

如何修改密码呢?也是通过给NickServ下达SET PASSWORD指令的(SETPASS是用于重置密码的)。
/msg NickServ set password mynewpass

仔细看命令,会发现怎么不需要提供当前密码呢,不符合Web的操作习惯啊。
那是因为IRC是直接依据当前会话的有效性为依据,判断是否允许修改的。如果当前登录会话已经超时,
修改密码就会提示当前用户未登录,类似于:
You are not logged in.

此时需要重新登录:
/msg NickServ identify curpassword

5.IRC 经常使用的命令

进入频道(注意前面的斜线和后面频道的#号都不能缺少, 比如进入Java的频道就要写 /join #java)
/join #频道名

连接服务器Server:
/server irc.freenode.net     #连接到freenode
/server irc.mozilla.org      #连接到moznet

查看某人资料(可以查到该user的ip地址以及所join的频道):
/whois 昵称

查看某IP登录的所有用户:
/who ip

离开频道, 并留下原因
/part #频道名 离开频道的原因

用来退出服务器, 并附上退出的原因
/quit 退出的原因

暂时离开: 使用away命令, 这样别人和你私聊的时候会收到away的系统提示, 如果退出暂离状态, 可以使用 /back 命令 
/away 原因

私信某人(不会打开新窗口)
/msg 昵称 要说的话 

私信某人(会打开新窗口), 也可以右键点击左侧聊天list中的某人后, 点击Open Private Chat, 效果一样
/query 某人昵称 []私信内容(可省略)>

/mode yourname +x 隐藏你的真实ip地址(进入channel前使用或者加进你的options>perform中。这样你就具有避开IP攻击的初级能力了。
/pass 密码 输入密码通过系统检查。如果是注册名字不在60秒内输入密码,系统会将强逼使用者换名。
/nick newname 改名
/ns set kill on 要求系统检查个人密码,并将冒名者杀掉。这是一个设置项。
在任何窗口输入这个命令,但事先你要先有/pass 密码,这样你的名字处于: This user has enabled nick kill enforce.

/ns ghost nick pass 杀掉你本人进程中断而停留服务器的名字或别人侵犯你的名字专用权时使用。
/list 列出所有的房间列表
/channel 这个命令需要在房间的大厅执行,它将打开一个房间的对话框,里面有标题设置栏,办(ban)列表,和房间模式.
/query nickname 开其他人小窗,也可以双击对方名字。
/query kkkkk 这样就开了kkkkk的小窗,你也可以这样: /query kkkkk 你好吗? 这样一开小窗就说了"你好吗"这句话了。
/Ignore nickname 把你讨厌的人忽略了。这样他说的话你一句都听不到。
/topic #channel newtopic 更改聊天室房间的主题。

 

done!

以上是关于golang 一个简单的命令行聊天室,如irc的主要内容,如果未能解决你的问题,请参考以下文章

Twitch IRC 聊天机器人成功连接但未检测到命令

IRC

如何使用 Python Twitch IRC Bot 获取聊天消息参数?

尝试IRC & freenode

连接到 Twitch IRC 聊天

irc使用教程