dawnwords/irc_server
Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Repository files navigation
IRC_SERVER设计 1 数据结构 1.1 用于存储客户端信息的数据结构user user数据结构中包含user_name(用户名), host_name(客户端主机名), server_name(登陆服务器名), real_name(真实姓名), nick_name(会话昵称)等基础用户信息字符串的指针,同时还有一个指向该用户所在频道数据结构channel的指针。 服务器端维持一个长度为FD_SETSIZE的user指针数组的全局变量user_table,每一个客户端发来请求会在服务器端非配一个文件描述符cliendfd,于是服务器则为该clientfd分配在Heap上给user数据结构,存储该clientfd的相关信息,随后将该内存的指针存到user_table[clientfd]的位置上,这样做是为了通过clientfd方便得获取user相关信息。 1.2 用于存储频道信息的数据结构channel channel数据结构本身是一个双向链表节点的结构,其中包含有分别指向前一个和后一个channel数据结构的指针,另外还包括一个记录频道名称的字符串指针,同时每个频道需要有一个记录频道成员的列表,项目中采用了一个单链表的结构存储频道成员的clientfd,这个单链表头元素用于存储当前链表元素个数,即频道成员数。channel链表的头header和尾footer作为全局变量,方便添加和删除channel 2 逻辑实现 2.1 总体概述 参考CSAPP教材中Echo Server的实现,IRC_SERVER可以通过类似的select机制通过IO复用获取客户端的请求,当select函数检测到p.readyset中有clientfd准备好与服务器通信后,check_clients函数被调用,该函数遍历所有clientfd,对 准备通信的调用rio_readlineb读取客户端发来的消息,此时如果rio_readlineb返回正值,则说明服务器成功从客户端读取了数据并存于缓存中以便后续处理,如果返回非正值,则说明客户端关闭,需要对服务器端对应于该客户端的数据进行垃圾回收处理,此时如果发现用户已加入频道,则需先退出频道再完成垃圾回收。 对客户端发送到服务器端的信息进行处理主要是第二阶段完成的任务。首先将缓存区域转为字符串,再调用tokenizer函数将字符串切割好并返回用户输入参数的个数,然后置于一个全局的字符串数组中,数组的第零项的字符串则可认为是用户想要执行的指令,根据指令的不同可知所需参数的个数,与tokenizer返回值比较则可判定参数个数是否合乎要求。随后根据不同的指令,调用不同的command函数并传入对应位置的参数则完成了消息的分发处理。 2.2 八个基本命令 2.2.1 USER指令 UESR指令需要将用户传递过来的user name和real name参数记录下来。在介绍user数据结构的时候提到这个数据结构是随同clientfd被服务器获知而创建的,创建的同时通过调用gethostbyaddr等系统函数将user数据结构的server_name和host_name填好,处理用户指令时,则无视用户在这两个参数上的输入。项目需求文档中并没有对user_name和real_name的格式做过多要求,对应的user_command函数也只需在用户是否已经调用过USER指令上进行判断,实现起来比较容易。 2.2.2 NICK指令 NICK指令是将用户传来的nick name参数记录下来,文档中对nick name的格式做出了详细的描述,不符合规定的需向客户端返回ERR_ERRONEUSNICKNAME的错误信息,如果参数合法,则遍历服务器端所有user的数据结构,查看该nick_name是否已被注册,是则给出用户反馈,否则为当前clientfd对应的user数据结构的nick_name项赋值。 需要注意的是,根据测试脚本来看,前两个命令同时达成时需要返回MOTD字符串,为了防止用户完成注册后重复输入上述两个命令使得MOTD不断显示则需要在两个command函数中对现有注册状态进行判定,如果只完成了另一方,则在完成本次command后显示MOTD,反之不显示。 2.2.3 QUIT指令 QUIT指令无需参数,需要注意的是一些退出工作和垃圾回收事项,首先判断用户当前是否加入频道,即located_channel是否为空,如果不是,则需先退出该频道。然后需要将用户的数据结构Free,因为user结构中都是以指针形式存储的,因此需要将其中字符串指针Free,同时清空pool结构中connfd在readset中所在位的值,并将对应clientfd数组值置为-1即可。 2.2.4 JOIN指令 JOIN指令接受一个参数,及想加入或创建的频道名称,这个参数的要求与NICK参数类似,不合法则想用户返回对应错误信息。该command函数依赖于create_channel函数的实现。create_channel根据给定的名称,遍历所有channel所在的全局链表,如果发现名字匹配的,则直接返回该channel指针,如果没有匹配,则在Heap上分配新的channel空间。需要注意的是,根据说明文档要求,如果用户已存在于某个频道中,则需要退出该频道,再加入新给定的频道。create_channel后,还需要将用户加入channel的member中,随后遍历整个member将频道中用户的列表返回,同时通知所有频道中已有的用户关于新用户加入频道的信息。 2.2.5 PART指令 与JOIN指令类似,PART指令也需要接收一个频道名的参数,该指令对错误信息的要求包括PART一个不存在的频道,PART一个不是自己所在的频道。处理好错误信息验证后,则将该用户的connect fd从频道member中移除,随后针对member新的size做判断,如果size为零,证明频道成员已经退光,则需将频道从全局链表中移除,并进行Free操作,如果zie不为零,则通知所有成员当前用户已退出,随后清空用户信息数据结构中located_channel指针即可。 2.2.6 PRIVMSG指令 PRIVMSG是用来向指定用户和频道发送消息的,需要接收两个参数,一个是发送的目标,一个是发送的内容,其中发送的目标可以为一系列由逗号分隔的字符串,所以,实现该command函数的第一步就是将字符串按逗号分开,strsep函数起到了这样一个作用,针对分离出的每个目标,需进行如下判断,如果该目标是一个频道格式的字符串(以#或&开头且长度小于9)则调用find_channel函数根据名称寻找相应频道,如果找到则向该频道发送消息;如果该目标是一个用户昵称格式的字符串,则调用find_user函数根据用昵称查找用户,如果找到,则向该目标发送消息,如果两次都没能找到,则返回ERR_NOSUCHNICK错误信息。 2.2.7 LIST指令 LIST指令用于展示当前所有频道的名称以及成员数量,基于现有数据结构实现起来比较简单,遍历一下全局的频道链表即可得到相应信息。 2.2.8 WHO指令 WHO指令是用来查询用户或频道信息的,指令接受一个参数,对应的command函数则首先根据参数find_channel,如果找到,则在最终结果的字符串缓存中加入相应频道用户的信息,如果没找到,则遍历所有的用户,如果有用户的名称信息有能与参数匹配的,则将其信息整合存入返回结果缓存中,完成遍历后,将结果返回给用户。 3 健壮性加强 原本tokenizer函数是存在bufferoverflow的问题的,即如果一个string token的长度大于MAX_MSG_LEG时,一个token单元就无法容纳如此大的内容了,所以我在函数memcpy时判断了next-current的长度,如果长度大于MAX_MSG_LEG则只允许复制MAX_MSG_LEG长度的内容,后面的strcpy也换成了strncpy,限制复制的最大长度为MAX_MSG_LEG。