专栏首页电子技术研习社Linux笔记(11)| 网络编程之自己动手写一个服务器和客户端

Linux笔记(11)| 网络编程之自己动手写一个服务器和客户端

今天分享的是比较有意思的东西——在Linux下通过网络编程实现一个简单的服务器和客户端。通过这个服务器和客户端,用户可以相互收发消息,类似于QQ或者是微信这样的社交软件,当然,从功能上来说只是实现了简单的通信,肯定是不可能像QQ或者微信这样那么强大,但是,对于学习网络编程来说,能实现这样的功能已经足够了,毕竟不是真的要做一个社交软件。

先来说一下这个程序实现的功能,这里就直接把我写程序时的更新日志放在这里好了:

version0.0:

功能:

初步实现了多个客户端与服务器的连接,并且服务器与客户端之间可以正常通信。

存在问题:

只能客户端与服务器通信,客户端之间不能通信。

version1.0

功能:

在上一版本的基础上,实现了客户端之间的通信,比如两个客户端之间可以收发消息

存在问题:

这个版本虽然实现了客户端之间的通信,实际上是客户端发消息到服务器,服务器再转发到客户端,但是没有做到用户可以向指定的客户端发消息,也就是说在服务器里写死了消息转发给谁,用户没有决定权。如果要实现定向发送,需要给每个客户端配一个账号,用来唯一标识,在这个版本里暂时没有。

version 2.0

功能:

在上一版本的基础上,为每一个客户端添加了ID账号,这个ID账号是用户执行客户端程序的时候输入的,通过这个ID号,用户可以向指定的客户端发送消息,经测试,收发正常。

存在问题:

1、当用户执行客户端程序的时候,需要及时输入登录的ID账号,如果用户没有输入,服务器将会被阻塞,以至于不能处理其他客户端的事情。

2、不能防止两个客户端使用相同的账号登录

version3.1

功能:

在上一版本的基础上,解决了同一账号重复登录的问题。

存在问题:

1、暂时没有解决version2.0里提到的第一点问题

2、如果第二个客户端的登录账号和第一个客户端相同,那么会被退出程序。但是即使下次使用别的账号登录,也只能由客户端1向客户端2发消息,客户端1收不到客户端2的消息。除非一开始两个客户端没有使用相同账号登录。

这也是我第一次在写程序的时候顺便写了一下更新日志,这应该是一个好习惯,因为在写程序的时候往往是不可能一次性写的很完美,都是在反复的修改过程的当中去完善它,因此,有必要记录每一次更新的变化,在每次重大更新之前都把程序先复制一份,可以防止后面不小心把前面写好的程序改的不能用。

好了,言归正传,来具体说一下这个程序的实现思路,这个程序分为服务器和客户端两大块,来逐一介绍。

服务器端工作流程:

(1)调用 socket() 函数创建套接字 用 bind() 函数将创建的套接字与服务端IP地址绑定

(2)调用listen()函数监听socket()函数创建的套接字,等待客户端连接 当客户端请求到来之后

(3)调用 accept()函数接受连接请求,返回一个对应于此连接的新的套接字,做好通信准备

(4)调用 write()/read() 函数和 send()/recv()函数进行数据的读写,通过 accept() 返回的套接字和客户端进行通信关闭socket(close)

所以了解了工作流程之后,编程只要围绕这几个函数的用法展开就行了,实际上只要搞清楚这几个函数的输入参数和放回值,基本上就成功一半了。关于这几个函数的用法,如果要展开来说的话,那么需要非常大的篇幅才能讲清楚,因为每个函数的输入参数都是比较复杂,这些输入参数里面有的甚至结构体里嵌套结构体,要想搞清楚结构体里每个成员的作用不是一件那么容易的事情,这里直接推荐几篇优秀的博文:http://c.biancheng.net/view/2123.html,在这个博客里详细介绍了网络通信的各个函数的用法。所以我就不再赘述。

把上面几个函数搞清楚之后,服务器基本上就搭建起来了,只是还没有添加什么功能。那么服务器暂时先放在这里,接下来穿插讲一下客户端。

客户端工作流程就简单多了:

(1)调用 socket() 函数创建套接字

(2)调用connect()函数来连接服务器。

这样写完之后客户端实际上已经可以和服务器通信了,也就是我上面说到version0.0,那么接下来介绍如何过渡到version1.0.

在version0.0里面,我们的客户端可以连接服务器,也可以相互通信,但是如果你再打开一个客户端,会发现根本连不上去,这时,我们可以使用select函数实现多路复用,多路复用,顾名思义,就是说各做各的事,标准输入事件到来,有相关函数处理。服务器处理服务器的事件,客户端到来时有相关函数对其进行处理,通过select遍历各fd的读写情况,就不用担心阻塞了。

程序去select的时候,如果没有数据输入,程序会一直等待,直到有数据为止,也就是程序中无需循环和sleep。使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时 不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。

关于select函数的用法同样很复杂,所以这里不展开来讲,推荐一篇博文:

https://blog.csdn.net/leo115/article/details/8097143

使用了select函数之后就实现了监听多个客户端,又不会被阻塞。当然,这里还有一个问题,就是客户端要能发能收,这两个该怎么协调?你不能直接使用while(1)里面写一个发送,一个接收,因为接收是一个阻塞的函数,如果收不到数据就一直阻塞在那里,那么发送就会被阻塞,想发送也就发送不了了。该如何解决这个问题呢?可以创建一个子进程,然后父进程和子进程一个用来发送,一个用来接收,这样因为发送和接收在两个不同的进程里面,不会相互干扰,这样就成功解决了这个问题。这样就实现了version1.0的功能。

但是这里还存在一个问题就是不能定向发送。正如日志里所说,要定向发送,需要为每个客户端配一个ID,这个ID相当于是登录账号,用户在每次执行程序的时候,都必须要首先输入登录账号。而且得把你的ID告诉服务器,服务器创建一个数组IDPool[]用来保存好各个客户端的ID,方便数据的转发。另外,要实现定向发送,就要求用户在发送数据的时候要告诉服务器向谁发送,就好比你在使用QQ或者微信聊天的时候,需要选择一个好友一样。

那么这个时候数据就变得复杂了,因为不仅仅要发送你实际的数据,还要发送你的目标联系人给服务器,这时,用结构体来封装数据比较合适,数据填充在结构体里面,然后一次性把整个结构体发送出去。服务器接收这个结构体,并且对结构体内容进行解析,解析完之后就知道了要发给谁,发送内容是什么。

要实现定向转发,还有一个问题要解决,就是对于用户来说,只知道对方的ID,但是对于服务器来说,只知道各个客户端的文件描述符,那么如何通过ID来找到对应的文件描述符就成了问题,找不到文件描述符就无法发送。这里,我就借鉴了uC/OS的思想,采用数组的方法,将文件描述符和ID账号联系起来,比如我们创建一个数组叫做ID_FD_Pool[],当客户端连接服务器的时候,服务器就已经知道了它的文件描述符,这时客户端向服务器发送ID,假设ID是5,那我们就可以给ID_FD_Pool[5]=fd,赋值上它的文件描述符,换句话说,通过这个数组,知道了ID,就能找到fd,这样服务器就可以准确发送了。这就是version2.0实现的功能。

这时自然而然会想到一个问题,如果两个客户端使用相同的账号登录会怎么办,服务器该怎么处理。那么正常情况下,都是不允许这样的情况发生的,所以应该在程序防止这样的情况。

怎么防止呢?方法就是服务器用一个数组来保存用户的ID,当有用户登录的时候,先接收它的登录ID,然后将这个ID和数组里的进行比对,如果没有找到相同的,就给客户端回复一个OK,客户端如果不能收到这个OK,就表明重复登录了,客户端直接退出进程,收到OK才往下执行。还有一个问题就是当客户端退出的时候,服务器应该要将这个ID从数组里面清零,否则下次再使用这个ID登录,还会被误认为是已经登录了,前面讲到服务器将客户端的ID保存在一个叫做IDPool[]里的数组里,但是怎么知道这个退出的客户端的ID在IDPool[]里的位置呢?还是上面uC/OS的思想,客户端退出的时候,知道它的文件描述符,因此可以创建一个数组并且赋值FD_ID_Pool[client_sock_fd]=i,将文件描述符和ID在IDPool[]里的位置绑定起来,这样通过文件描述符就可以找到ID在IDPool[]里的位置,然后将它清零。这都是一些技巧。这样的话就实现了version3.1的功能。

这样的话就基本实现了想要的功能,经过测试,可以在多个客户端之间相互发送消息,消息类型支持字母,数字,汉字等,对于普通的交流已经足够了,当然程序肯定也存在很多不足,一些bug可能没被测试发现,需要持续的改进。

来看一下效果:

实际上程序的优化是没有尽头的,看有没有这个需求了,如果只是通过这个来熟悉网络编程,那么做到这种地步就可以点到为止了,有时间的话可以去继续优化。

当然,如果直接看我上面写的内容,可能会看不懂,必须要先了解网络编程的一些基础知识,可以参考我前面提到的链接先学习,时间有限,我上面讲的都是一些思路,具体的实现还是要对照代码来理解。

本文分享自微信公众号 - 电子技术研习社(zjf18770701843),作者:小小飞飞哥

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Linux笔记(8)| Shell脚本编程

    command1 && command2 当command1为假时才执行command2

    飞哥
  • uCOS | 软件定时器

    硬件定时器是芯片本身提供的定时功能。一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中...

    飞哥
  • Linux笔记(9)| 一步步深入Makefile

    今天分享的是如何一步步深入地学习Makefile。在Linux中编译代码,不像是Windows中有很多集成的IDE,Linux中都是通过基本的编译工具如gcc来...

    飞哥
  • Zookeeper术语介绍

    在之前的文章Zookeeper体系介绍中我们介绍了Zookeeper体系。因此,在深入了解ZooKeeper的工作之前,我们必须了解ZooKeeper中的一些术...

    后场技术
  • SOSE25 Process-Oriented System Analysis Process Mining

    Process-Oriented System Analysis Process Mining

    安包
  • 「小程序JAVA实战」zookeeper简介(71)

    IT故事会
  • Linux下开启FTP的21端口

    这几天一直在学习在CentOS7.0创建本地yum源和局域网yum源,准备两台CentOS7.0虚拟机,一个做服务器,一个做客户端;由于开发环境只有局域网,没法...

    麦克劳林
  • dedecms总是被黑怎么办

    阿里云ECS服务器是目前很多网站客户在使用的,可以使用不同系统在服务器中,windows2008 windows2012,linux系统都可以在阿里云服务器中使...

    网站安全专家
  • 分布式基础__HTTP 通信协议

    https://www.zhihu.com/question/44323871/answer/347628315 这个地址是知乎上的一个问题,

    矿泉水
  • Power BI中常规切片器的使用方法及视觉效果

    切片器是最常用的,也是几乎必不可少的视觉对象,基础的切片器根据值的类型不同所体现出来的视觉效果也不一样。

    逍遥之

扫码关注云+社区

领取腾讯云代金券