[网络]——浅析网络套接字

套接字

非常开心,笔者经过漫长的Linux系统编程终于开始了网络编程的旅途,更让人开心的是,今天我们所要将的内容虽然是与网络相关的,但是他与我们的系统编程密不可分,你发现你用你所知道的知识居然也能做出来一些好玩的东西。我们今天主要讲解的是Linux下的网络套接字编程,最后我们会用这些接口来实现一个简单的C/S通信程序。

套接字概念

TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点就叫做套接字(socket)或插口

这里出现了几个陌生的概念,第一个是Tcp,Tcp是一种网络传输层的协议,我们暂且不谈Tcp协议是啥,本篇博客我们其实还是围绕函数接口来进行讲解。第二个是ip地址,相信大家对于ip地址并不陌生,你可能具体不知道他是做什么的,但是你只要知道他是标识网络中的主机的。第三个是端口号的概念,对于本节内容,我们有必要为大家阐述什么是端口号。

端口号实际上是我们上面提到的Tcp协议中的一部分,后面的博客给大家讲解。端口号主要有以下的特点:

  • 端口号是一个2字节16位的整数
  • 端口号用来标识一个进程,告诉操作系统,当前这个数据要交给哪一个进程来处理
  • ip加端口号能标识我们网络中某个主机上的唯一一个进程,ip加端口号也就构成了套接字的概念
  • 一个端口号只能被一个进程所占用

看了上面的概念之后我们其实可以明白,端口号其实就是一个标志,它可以标志某个主机上的一个进程。但是有的同学疑惑就来了,那我们之前不是有一个进程id的概念么,他们之间有什么区别呢?举一个例子,你是某野鸡大学的学生,你大一开学时学校给了你011的学号,但是这一年你不学无数,好吃懒做,从不上课被退学了,然后经过一年"努力",你居然又上了这所大学,这次学校分配给你了一个012的学号,但是你的身份证在你的一身中从不会被改变。端口号也是同样的道理,一个进程可以有多个端口号,但是他只能有一个进程id。

简单认识TCP and UDP协议

TCP协议:

  • 传输层协议
  • 面向链接
  • 可靠传输
  • 面向字节流

UDP协议:

  • 传输层协议
  • 无链接
  • 不可靠传输
  • 面向数据报

上面tcp和udp的简单介绍中,你其实什么也不用关心,但是你必须要知道Tcp面向链接,Udp无链接。什么是链接呢?通俗一点来说就是你找了一个男(女)朋友,你脑海中本来有一个描述男朋友的结构体,然后你把你现在男朋友的信息填充进去,不管你男朋友走到哪里你都知道他是你对象,这其实就是链接。协议中Udp则不进行这种描述,反之Tcp需要进行建立连接,也就是为通信的双方构建描述双方信息的结构体。

套接字编程铺垫

那说了这么多套接字编程到底是干什么的,很多小白读者还是一头雾水。其实我们的套接字编程是为了让两个不同的网络主机可以实现通信,但是如果你仔细体会其中的道理我们会发现,我们需要通过端口号来标识进程,其实本质上是不同的俩个主机之间的进程在进行通信,所以归根结低,网络套接字编程从某种角度上说其实是进程间的通信

网络字节序

清楚了套接字编程是为了通信后,我们再谈另一个问题,我们前面在讲进程间通信时是一台主机上的俩个或者多个进程进行通信。通信就是为了收发数据,但是这里有一个棘手的问题,你应该还记得什么是主机大小端吧,如果你忘记了什么是大小端,那么戳笔者之前的博客链接主机的大小端。
既然我们每一个主机都有自己的大小端,那么如果从一个小端机器向大段机器发送数据那么必然会出现不可预测的结果。不过如果你直发字符串就没有问题,因为字符是一个字节么,不管怎么读取都不会出现问题。
那么如何解决这些问题呢?与其从我们接收方俩端进行数据格式的重复转换,不如我们直接定义一个网络传输字符的规定,以后大家都用统一的大小端模式进行首发数据不就行了,所以这里就出现了我们的网络字节序的概念。Tcp/Ip协议通一规定网络数据流采用大端字节序,也就是低地址高字节。

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主字节序的转换。这些函数名你囐一看感觉很很恶心,但是其实他们很好记住,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
[网络]——浅析网络套接字_第1张图片
ps:笔者有时候看Linux的系统调用能给读者看吐了,但是谁让人家是系统调用呢,毕竟是底层的东西,越底层速度越快。这也就是有时候老铁们常调侃java的运行速度比c++慢的原因。java之所以好用就是封装的到位,但是速度相对来说会慢,c++速度快但是还在用指针这些底层难用的东西,不过笔者还是要小声bb一句。。。c++ forever(破音)。

socket编程接口详解

到这里,我们算是到了本博客的核心部分了,之前我们说了网络套接字编程进行主机之间的通信实际上也是进程间的通信,很多通学这里还是不太理解,那么我们先介绍一个函数,通过对这个函数的介绍我们再一次深入的理解socket编程本质。

创建socket对象函数

话不多,先把函数头给大佬们抬上来:图有点小,点开看大图
在这里插入图片描述
参数什么的先丢一边,睁开眼睛同学们,看看这个函数返回了什么。没错,他居然返回了一个文件描述符,你一定还记得匿名管道怎么通信的吧,匿名管道通过调用函数返回读写文件描述符,其实相当于写端将数据写到文件中,读端从这个文件中读出数据。而socket对象创建函数惊人的相似,他们的通信原理基本相同(只是原理相同),到这里你也就明白为什么我们要说主机通信其实是进程间通信。

  • socket是实现网络通信主机进程间通信的一种机制。从用户空间来看,socket就是一个文件描述符,对socket的操作等同于对普通文件的文件描述符进行操作,也就是说我们可以使用read,write,close等函数进行操作。一旦双方对该socket都进行初始化后,对端的数据交互都是通过该socket实现的。
  • 而从内核空间来看,socket不在简单的指向一个磁盘文件,相应的读写指针指向的代码亦是网卡驱动程序提供的数据发送或者数据接收函数。其主要资源是一个内核内存空间的struct sk_buff结构体对象。该结构体中描述了双方的基本信息,缓冲数据等。

现在你一定对网络通信编程有了更深一步的理解,到此时我们所有的铺垫全部做完,现在开始介绍网络编程的接口,我们本次讲解使用的是基于BSD的通信编程接口,这些接口隔离了下层的实现,只需要调用相应的接口就可以。
此时我们在做最后一步的铺垫,我们使用生活的一个例子来为大家进行讲解socket网络编程,因为系统调用还是过于生涩,不好理解,使用栗子能帮助我们更好的理解这些接口。

大家在生活中拨打电话应该是一个很常见的情景,那么我们用拨打方和接收方作为栗子来描述一下场景:

[网络]——浅析网络套接字_第2张图片
现在我们使用上面接听者拨打者栗子来给大家讲解我们套接字编程中的一些列接口。

在这里插入图片描述
在Linux操作系统中,要实现socket通信,通信双方都需要建立各自的socket对象,也就是本函数的返回值。当我们此函数调用成功之后,该socket对象没有绑定任何的端口号和ip信息,用我们上面的拨打电话的栗子来说,调用函数就相当于买了一台电话机,但是此时我们还没有绑定电话号码,所以还不能通信。

  • 参数1:参数一填写的是socket对象所以使用的地址簇,也就是此对象使用的协议类型,这里我们经常使用的有AF_INET表示是ipv4协议簇,PF_INET6表示ipv6的协议簇,PF_LOCAL表示本地通信。我们只要记住这三个即可,其余笔者查阅资料大多数文档中表示其他协议簇在Linux中目前还没有实现。
  • 参数2:第二个参数表示socket的类型,也就是要填写的套接字类型,这里就列举我们最长使用的三种:为流套接字类型SOCK_STREAM、数据报套接字类型为SOCK_DGRAM、原始套接字SOCK_RAW。从名字对比我们上面的Tcp/Udp简单介绍我们就知道Tcp使用流式套接字,Udp使用数据报套接字。
  • 参数3:第三个参数具体让你选择协议簇中的一种协议,这里大多数情况我们设置为0,让系统自己选择默认协议,但是原始套接字需要指定具体的协议

此时我们套接字对象就创建完了,我们现在相当于买了一台电话机,但是电话机还没有电话号码,所以下一步我们就需要做一些信息绑定工作。

绑定本地ip和端口

在这里插入图片描述
这里参数一很明确就是我们socket函数中的返回值,也就是你需要绑定的文件描述符对象,但是参数二却是一个结构体指针,这个参数需要我们好好理解,我们下面详细介绍,参数三也比较简单,就是第二个参数结构体的大小。

现在我们来研究一下第二个参数,上面说了,我们的绑定函数是为了将套接字对象和ip和端口号绑定到一起,所以这个结构体中的变量肯定有描述ip和port的,但是我们仔细思考一下,接触过ip地址的小伙伴都知道ipv4标准的地址是32位,ipv6的ip地址是128位,然而这里的结构体指针居然只有这一种,那如果我使用不同协议怎么处理呢?dont worry,聪明的编写者使用了泛型编程的思想,你传入的结构体类型可能会不同,但是你只需要将你的对象转换成参数二类型的指针,他在底层自动帮你转换。那么怎么实现的呢,一起来看张图:下图中传入时的指针都是相同的,但是传入后会根据16位地址类型进行区分转换
[网络]——浅析网络套接字_第3张图片
下面不妨我们看看我们本篇博客要使用的sockaddr_in结构体内部是怎么样的:
sockaddr结构也就如我们下图所示,common字段中我们需要填写sin_family,这里我们填写AF_INET。
[网络]——浅析网络套接字_第4张图片
这里common与上面相同,第二个变量是端口号,第三个变量是一个结构体,结构体中需要我们填充ip地址
[网络]——浅析网络套接字_第5张图片
此结构体变量中我们需要填充32位的ip地址:
[网络]——浅析网络套接字_第6张图片
但是这里我们又发现了一个问题,结构体中的ip地址是int类型的,但是我们看到的ip地址通常都是"127.0.0.1"类似的形式,所以也就意味着一定有一组函数可以实现int类型和点分十进制ip转换:

字符串转in_addr函数,这三个函数中随便挑一个就行,第二个是最简单使用的,直接把字符串丢给他就行:

int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
int inet_pton(int af, const char *src, void *dst);//参数一填协议簇

in_addr转字符串函数,第一个也很好用但是存在一定的问题,他返回一个char*的串,也就意味着他返回了可能返回了局部变量,但是查询文档后发现这个返回值开辟在静态区上,所以如果你下一次调用就会覆盖上一次的结果,是线程不安全的
在这里插入图片描述
所以这里一般推荐使用第二个函数能更好一些。

char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);//参数34分别是字符缓冲区和大小

到现在我们只需要在绑定时创建一个sockaddr_in结构体,然后将相对应的字段分别填写后传给bind函数即可。此时我们就好比申请了一张电话卡并且与手机绑定。

监听网络

此时我们准备工作就做好了,但是现在我们还是不能通信,对于我们的接听端来说,你现在还必须将手机设置为监听状态,否则还是无法收到拨打者的电话。
在这里插入图片描述
第一个参数同样是我们上面的文件描述符不用多说,第二个参数我们需要详细的讲解一下:

这个参数是一个int型,也就是你需要传给他一个整型数字,但是你需要知道的是这个数字不能太大,此时我们的接听端可以理解为是服务器,所以拨打者是客户端,使用官方的话来说,当有多个客户想链接服务器时,此值表示可以使用的处于等待的链接队列长度,其实也就是一个已经完成三次握手成功建立连接的请求,这么说你可能懵了。那么换一个打电话的栗子吧。

你打电话时肯定遇到过占线的情况,如果你打给对方时刚好有人也在和对方聊天,那么就出现了占线,但是此时电话并不会直接让你挂掉而是提示你等待,所以等他们结束通话,你就可以立马与对方通话,这也就是为什么参数不能太大的原因,等待的人总不能拿着电话排队一下午吧。但是这里还是理解的太过于肤浅,我们后面还要更深刻的理解这个参数。
[网络]——浅析网络套接字_第7张图片

客户端发起链接

现在接听端已经一切准备就绪,等待拨打者拨打电话。
在这里插入图片描述
我们发现发起连接的函数参数和绑定的参数完全相同,但是他们的意义的不同之处在于,绑定的时候你绑定的ip和端口号是自己本地的,而连接的时候你填充的应该是你所要连接的那个人的。用打电话的栗子也很好理解,绑定肯定绑定自己的电话号码,当你给别人打电话的时候你一定使用的是他的电话号码。

服务器接收连接

拨打者现在已经拨打电话,接收者已经处于了监听状态,所以此时服务端收到这个电话就会立即处理:
[网络]——浅析网络套接字_第8张图片
看到参数时有的老铁暗暗窃喜,参数居然又和上面的相同,但是请你擦亮眼睛哦,第一二个参数还是没变,但是第三个参数变成了一个指针,我们这里是要拿到别人的结构体长度,所以你就理解为这是一个输出行参数,但是描述的还是长度。然而这里的参数并不是本函数的重点,来看看函数的返回值,你一定会感到惊讶:
在这里插入图片描述
这里居然又返回了一个文件描述符,什么鬼,上面不是已经有一个了么?原理是这样的,我们上面其实已经初步介绍listen函数会维护一个等待队列,这个队列中是已经建立连接的客户端,然而我们真的要处理这些请求就需要让accept函数将这些等待队列中的请求一个个的拿出来。这里说一个其他的栗子,就比如你去餐馆吃饭,门口总有一个人在拉客,但是他们拉到客之后自己并不处理,而是交给饭店中的小二处理,这里listen函数个accept函数也是相同的道理,所以我们从这开始把上面的套接字叫做监听套接字,而accept返回的新套接字称为当前读写套接字对象。

到此,只要accept取出一个请求处理,也就完成了连接,此时我们便可以开始交流发送数据了,所以回到我们的套接字编程,你会发现这里有一个四元组在建立连接时始终保持着非常重要的角色,他们分别就是源ip,目的ip,源端口,目的端口。

再强调一点,这里accept函数的使用时,明显第二个参数是需要得到远端的信息,所以我们创建一个sockaddr_in对象传进去就好。

发送接收数据

此时已经建立连接,我们可以直接开始互相发送数据了,我们一直说socket对象其实就是一个文件描述符,所以也就毋庸置疑这里绝对可以使用read,write函数读写,这里我们就不在做过多的介绍,我们的BSD标准也为我们提供了功能更多的发送接收数据函数调用。
在这里插入图片描述
在这里插入图片描述
这两个函数的前三个参数和read,write函数完全相同,只有第四个参数flags不同,如果将这个flags设置为0,那么效果还是同read,write函数,所以一起来看看flags有哪些选项,哪些作用。其中不阻塞使用的应该是最多的。

#define MSG_OOB 1                               带外数据
#define MSG_DONTROUTE                           本地不路由
#define MSG_DONTWALT                            不阻塞
#define MSG_WALTALL                             等待所有数据

关闭socket对象

这里也是一样的,你可以使用close函数来直接关闭socket对象,但系统也为我们提供了功能更多的函数:
在这里插入图片描述
Tcp协议是全双工的,也就意味着发数据的同时也可以收数据,但是使用close函数时将读写通道全都关闭了,有时候不够灵活

  • how选项传0时表示关闭读端
  • how选项传1时表示关闭写端,如果你觉得不好记忆把0想象成一张嘴,1想象成一根笔,这样就好记忆多了
  • how选项传2时与close的功能相同

获取本地socket以及对端的信息

在这里插入图片描述
在这里插入图片描述
这俩个函数分别获取本地和对端的信息,他们的返回值都是一个套接字,此套接字存储了你想获得远端和本地的ip以及端口号信号,感兴趣的通信可以了解一下这俩个函数,不过实际使用的不多。

Udp网络套接字

以上我们所介绍的接口其实是基于Tcp套接字编程的接口,本小结我们将介绍Udp套接字的接口,我们看到Tcp关于socket编程的接口已经非常多了,这里居然还有Udp的,不过不要担心,Udp大部分的接口都与Tcp相同,只有收发数据的接口不太相同,并且从网络通信的流程上来说Udp更加简单。

Udp套接字收取数据

在这里插入图片描述

  • 参数一:udp通信同样第一步需要创建套接字对象,所以调用的也是socket函数,只不过这次我们将套接字类型定义为SOCK_DGRAM,所以参数一就是socket函数返回的那个文件描述符
  • 参数二:收取数据的缓冲区,顺便说一下参数三其实就是数据缓冲区的大小
  • 参数四:这里的flags默认设置为0即可
  • 参数五:这里不用多说,其实就是我们要使用的不同套接字类型存放ip和端口号的结构体地址,最后一个参数是这个结构体长度的地址,值得补充的是,因为你现在是收数据,所以这里结构体将会拿到向你发数据对端的信息

Udp套接字发送数据

在这里插入图片描述
这里的参数一是socket套接字对象,参数二是你要发送数据的起始地址,参数三是发送数据的长度,参数4flags默认设置为0,参数四五也于接收大体相同,但是注意,这里的长度不在是地址。很多同学不知道这里函数的接口最后一个参数有时候传结构体的长度,有时候传长度的地址,这里其实记住一点,你发送给别人时就需要传准确的长度,而如果需要获取别人的信息时传的是长度的地址,不难理解因为只有在不知情的情况下才需要传一个输出型的参数给函数。
这两个函数是与Tcp套接字唯一不同的俩个接口,他们之间的差别只有接口调用时的不同,下面我们来整理一下他们调用的函数和顺序的流程图。

TCP and UDP套接字编程流程图

tcp socket编程流程图:
[网络]——浅析网络套接字_第9张图片
Udp socket编程流程图:

[网络]——浅析网络套接字_第10张图片
这里需要提的一个问题是,我们发现Tcp套接字编程中客户端并没有bind这一项,其实对于客户端来说他也是允许绑定的,但是绑定端口号和ip地址后,如果有一个服务器占用了客户端的端口号,那么这个客户端就再也无法启动了,不要因为打电话申请电话号码的栗子让你陷进去,栗子只是便于记忆,根本我们是为了如何应用这些接口。

写给已经非常了解网络socket的读者

上文中我们其实面向的知识还比较基础,但是面试中还有一部分关于套接字的知识我们是必须掌握的,下面我们先拿一幅图来看看如果与传输层结合起来,我们的socket网络模型是怎么样的,并且网络模型中我们还会衍生出一些新的问题:
ps :笔者的图都是使用Visio画的,感觉这个工具使用起来还是很舒服的

[网络]——浅析网络套接字_第11张图片
上图是一张与tcp三次握手和四次回收结合起来的图,图中具体各种的状态变化笔者并没有标出来,我们将这个函数与tcp协议模型结合了起来,我们大体的观察这张图能够看出从客户端的角度来看,socket函数为通信做准备工作,一旦调用connect就触发了tcp的三次握手与服务器建立链接,而与服务器建立链接成功后这个链接会放在listen函数维护的全连接队列中,accept函数阻塞等待建立成功的请求,一旦有请求建立accept函数立马将此请求从队列中拿出然后立刻返回处理,此时我们的建立便成功了,当服务器和客户端任意一方调用close函数时,也就触发了四次挥手,每一方关闭就是一次两次挥手一共四次。

从上图中分析的角度来看,我们所写的套接字代码都是建立在应用层,现在我们要透过应用层来解决一些更底层的问题

问题1:如何解决端口被占用的问题

我们都知道一台服务器上的某一个进程需要绑定一个端口,其他主机需要访问这个进程时都需要通过这个端口,所以我们也就需要为我们的进程指定特定的端口,这里我们的问题是如何解决端口被占用的问题,问题的描述是:一台服务器为某个进程绑定了一个端口,但是由于某种原因这个服务器宕机了,但是此时我们再次使用这个端口重启服务器时会报错绑定失败,因为Tcp的四次挥手时还有一个time wait状态,也就意味着此时端口还在被占用,试想如果这是淘宝双十一时那该造成多大的损失啊,所以为了解决这一问题,我们系统为我们提供了一个函数,当我们设置此函数时就不会出现绑定失败的情况了:
在这里插入图片描述
参数一为要设置的套接字对象,参数二默认设置为SOL_SOCKET,文档中好像只告诉了这一个选项,参数三就是你要设置什么选项,我们这里要设置的是SO_REUSEADDR,选项描述为:指定用于验证bind()提供的地址时使用的规则应允许重用本地地址。当然了,函数的option_name还有很多选项感兴趣的同学可以研究下,参数四我们这里设置为1的地址,最后一个参数设置为option_value的大小,函数的使用如代码所示:

int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

此时我们再让一个刚刚关闭的客户端再重启时就没有任何问题了。

问题二:如何随机的绑定端口

我们之前说过,客户端一般是不进行端口号绑定的,都是不指定让操作系统帮我们随机,原因是如果有多个客户端绑定同一个端口号时就会出现绑定失败的错误,从另一方面说客服端绑定端口号其实也是没有意义的,毕竟没有人会主动链接他。

回到我们的问题,怎么随机的绑定一个端口呢?其实服务器也可以随机的绑定端口号,你只需要把存放信息结构体中的sin_port选项设置为0,操作系统就会自动帮你分配一个合适的端口号给你,保证这个端口号没有被占用,值得强调的是,小伙伴不要把这里的随机真的理解为rand哪种随机,操作系统此处使用了一些算法来计算合适的端口。你要明白,其实计算机并没有真正以上的随机的。

问题三:如何随机的绑定ip

既然port可以不显式的绑定,那么ip一定也有办法,这里给大家介绍一个宏:INADDR_ANY,看起来很高大的上的宏其实也是0,所以你以后记住,只要是一提到随机,那么就一定要想到数字零。不过这里操作系统帮你绑定ip地址时一般是你主机的ip地址,不然客户端怎么通过ip连你呢?
我们前面说一般要把端号号转为网络序列,所以使用htonl,不过这个宏是0的话大小端也就不那么重要了。

问题四:Udp客户端能不能调用connect函数呢

Udp客户端其实也是可以连接服务器的,但是由于Udp是不是面向连接的,所以这里connect也就不会给服务器发送数据包,那是不是就意味着调用connect毫无用处?其实调用connect会让你发现意想不到的事情:

connect在调用时其实没有向外发包,只是在协议栈中记录了该状态,应该是生成了一个类似TCB的结构。之后如果发生网络异常,比如对端不可达,客户端在往对端写数据后,本机会收到一个ICMP回应,则回来的ICMP不可达的响应能够被协议栈处理,通知客户端进程;当客户端再次对该fd进行操作时,比如读数据时,read等调用会返回一个错误。而不调用connect时,对于返回的ICMP响应,协议栈不知道该传递给上层的哪个应用,所以客户端进程中捕获不到相应的错误。
有兴趣的小伙伴可以看看这篇博客,了解一下就好:socket中的bind问题

问题五:listen函数的第二个参数深入理解

我们之前只是非常粗劣的说listen第二个参数是已经完成连接的队列长度,但是这样听起来好像并没有什么说服力,那么我们现在就来做一个实验来验证一下:
我们把服务器的listen函数的第二个参数设置为1,然后让服务器死循环,然后使用客户端去连接服务器,然后使用netstat命令查看网络连接情况
[网络]——浅析网络套接字_第12张图片
由于我们的服务器和客户端现在在一台主机上,所以图中的相同颜色的连接对于对端来说其实是一条连接,我们仔细的观察图中的State,这里黄色和蓝色双方都是established的状态,但是黑色的却不太一样,客户端向服务器的状态是成功连接,而反过来则是SYN_RECV状态,也就是我们所说的半连接状态,当你过一会再次查看时发现这条连接被清理掉了。

我们看看半连接时服务器的状态:此时我们发消息没有任何回应,程序被阻塞在那里
在这里插入图片描述
还记得我们给listen第二个参数设置为1,但是本实验中有两条连接完全建立了链接,所以你可以理解为listen的第二个参数加一就是当前服务器能够建立全连接的个数。
[网络]——浅析网络套接字_第13张图片

问题六:recv和send函数阻塞和非阻塞

这个问题真的超级重要,这也是理解socket网络编程通信的重点和难点,可是笔者还没有学习高级io,这部分内容将会在后面更新,还请老铁们谅解。

使用SOCK_STREM实现Tcp服务器

以下是一个C/S模式简易通信端的实现代码,如果有同学想看看Udp的简易通信程序,笔者下面的github连接中都有

//server.hpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;

class TcpServer{
private:
	int listen_sock;
	string ip;
	int port;
public:
	TcpServer(string _ip, int _port) :ip(_ip), port(_port), listen_sock(-1)
	{}
	void InitServer()
	{
		listen_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (listen_sock < 0){
			cerr << "listen_sock error" << endl;
			exit(2);
		}
		int opt = 1;
		setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
		struct sockaddr_in local;
		bzero(&local, sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(port);
		local.sin_addr.s_addr = inet_addr(ip.c_str());//INADDR_ANY
		//假设我有两张网卡也都可以通过此端口进行链接
		if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
			cout << "bind error " << endl;
			exit(3);
		}
		if (listen(listen_sock, 5) < 0){
			cout << "listen error" << endl;
			exit(4);
		}
		signal(SIGCHLD, SIG_IGN);
	}
	void Service(int sock)
	{
		char buf[1024];
		while (1){
			ssize_t s = read(sock, buf, sizeof(buf)-1);
			if (s > 0){
				buf[s] = 0;
				write(sock, buf, strlen(buf));
			}
			else if (s == 0){
				cout << "client quit!" << endl;
				break;
			}
			else{
				cerr << "read error" << endl;
				break;
			}
		}
		close(sock);
	}
	void Start()
	{
		struct sockaddr_in peer;
		socklen_t len;
		for (;;){
			len = sizeof(peer);
			int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				cerr << "accept error" << endl;
				continue;
			}
			std::cout << " Get a new client " << sock << endl;
			pid_t id = fork();
			if (id < 0){
				cout << "fork error" << endl;
				close(sock);
			}
			else if (id == 0){
				//if(fork() > 0){
				//    exit(1);
				//}
				close(listen_sock);
				Service(sock);
				exit(0);
			}
			else{
				close(sock);
			}
		}
	}
	~TcpServer()
	{
		if (listen_sock > 0)
			close(listen_sock);
	}
};

//clinet.hpp

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

using namespace std;
class TcpClient{
    private:
        int sock;
        string ip;
        int port;
    public:
        TcpClient(string _ip, int _port):ip(_ip),port(_port),sock(-1)
        {}
        void InitClient()
        {
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if(sock < 0){
                cerr << "socket error" <> in;
                write(sock,in.c_str(),in.size());
                ssize_t s = read(sock, buf, sizeof(buf)-1);
                if(s > 0){
                    buf[s] = 0;
                    cout << "Server Echo # " << buf << endl;
                }
            }
        }
        ~TcpClient()
        {
            if(sock > 0)
            {
                close(sock);
            }
        }
};

这里就贴服务器和客户端类的实现把,还有server.cc以及clinet.cc代码我就不贴了,太长了,我直接丢给大家一个连接,我还实现了单进程,多进程,多线程,和线程池版本,大家有兴趣都可以看看。最终版本

总结

本篇博客大体介绍了socket网络编程的一些知识点,如果有同学想深入理解,可以看看官方的文档,本文有什么错误和大家有什么建议还请只管指出。

你可能感兴趣的:(计算机网络)