网络编程套接字

目录

理解源IP地址和目的IP地址

认识端口号

理解端口号 

当唐僧到了西天,目标就达成了吗?

数据刚开始的时候是从哪里来的呢?是在计算机上凭空产生的吗?

网络通信的本质

端口号的意义 

ip与端口号 

系统中进程的pid vs port(端口号)?

ip vs 端口

一个进程可以关联多个端口号吗?一个端口号可以关联多个进程吗?

理解源端口号和目的端口号

认识TCP协议 

认识UDP协议

简单理解TCP和UDP 

网络字节序

​编辑大小端的概念

socket编程

什么是socket 

socket的引入

socket的简介

socket的表示方法

socket 常见API

sockaddr结构

关于接口通用性的设计

为什么要设计这种结构呢?

简单的UDP网络程序

服务端(server)的编写 

创建套接字

作为一个服务器,要不要让客户知道对应服务器的地址(ip+端口)?

绑定接口

sockaddr_in 

填充sockaddr_in 

进行绑定

提供服务

recvfrom

sendto

客户端(client)的编写

创建套接字

使用服务

客户端需要显示的bind吗?

client指明的端口号,在client一定会有吗?

服务器也会存在这样的问题啊?

通过sendto发送消息

通过recvfrom接受服务端发回来的消息

程序测试 

启动服务端

​编辑启动客户端(如果没开发端口,那么用自己的服务器ip是无法进行通信的)

如果绑定云服务器的私网ip和公网ip分别会怎么样?

完整代码

关于上述代码细节性的问题

对于客户端

对于服务端

端口号的问题

测试

基于UDP实现一个简单的xshell

思路

如何执行该命令呢? 

对于服务端的修改

对于客户端的修改

结果展示

完整代码

udp_server.cc

udp_client.cc

简单的TCP网络程序

服务端

创建套接字

绑定

建立连接

什么叫做面向连接?

listen

提供服务

accept

tcp通信过程 

accept的返回值 

accept的后两个参数

客户端

创建套接字

绑定与连接

connect 

进行正常的业务请求

测试

完整代码

tcp_server.cc

针对上述TCP通信的扩展 

存在的问题

解决方案一

优化1.0

关于文件描述符

优化1.0进行测试:

优化2.0

解决方案二

测试:

多线程或者多进程的利弊 

解决方案三 

方案一和方案二存在的问题

方案三进程或者线程池

完整代码:

代码逻辑

测试

套接字总结

我们究竟在干什么?

TCP协议通讯流程(附)

​编辑服务器初始化:

建立连接的过程:

数据传输的过程:

断开连接的过程:

在学习socket API时要注意应用程序和TCP协议层是如何交互的:

TCP 和 UDP 对比


理解源IP地址和目的IP地址

ip地址,我们就谈论的是公网ip。ip地址(公网ip)唯一的标识互联网中的一台主机。

源ip,目的ip:对一个报文来讲,就是从哪来,到哪里去。一个报文从我的机器来,到对方的服务器去。一个报文从哪来,到哪去最大的意义就是指导一个报文该如何进行路径选择。

到哪里去:本质就是让我们根据目标,作为路径选择的依据。因为有了源ip和目的ip,所以才有了下一跳设备(Mac地址的变化)就好比唐僧知道自己去西天,当他到了女儿国的时候,女儿国国王告诉他,下一站应该去车迟国,唐僧就应该收拾行囊向西到达车迟国,这是因为他的目标是西天。
 

认识端口号

端口号 (port) 是传输层协议的内容 .
  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用

理解端口号 

我们把故事再进一步。这里有两台主机,两台主机之间有很多的路由器,主机A通过通信找到了目标主机。

当唐僧到了西天,目标就达成了吗?

答案是没有。站在报文的角度确实是完成了目标了,但是在应用的场景,唐僧到西天是没有太大意义的,因为唐僧到了西天,要见如来佛祖,如来佛祖提供了一套服务,提供经书,所以数据从A主机到达B主机不是目的。数据到目标主机B上的一个进程,进程提供数据处理的服务。唐僧就相当于报文,如来佛祖,就相当于目标主机上的一个进程,如来佛祖提供经书,就相当于这个进程提供的数据处理服务。

数据刚开始的时候是从哪里来的呢?是在计算机上凭空产生的吗?

计算机本身不产生数据,产生数据的是人!比如:我今天想点个外卖,我就得现在美团上看看有没有想吃的,有想吃的之后,我就可以直接下单,下单后填写地址及其付款。这种行为完全是人的行为。我想看抖音,抖音上的视频不是天然就有的,而是曾经有人录了抖音,我们现在在网上浏览的所有数据都是人产生的。人是通过特定的客户端,产生数据的。我今天想上B站,看一个特殊的视频,你就得登录,可我没有账号,我就得注册,注册完后进行登录。所有的数据产生在起始点都是人产生的,是人通过客户端产生的数据。所以当我刷抖音的时候,实际上在人的角度是我在刷抖音,在技术的角度是你的抖音APP,比如你的手机是主机A,你手机装了一个抖音APP,抖音不断的向我们的服务器中的抖音服务,请求各种短视频。

网络通信的本质

站在普通人的角度,就是人和人之间通信。

技术人员的视角,所有的网络通信,本质上述是进程间的通信。

比如:抖音的APP客户端和对应的抖音的服务器。这个抖音的客户端在启动之后在OS上就是一个进程,抖音的服务器也是一个进程(比如你可以24小时都在刷抖音,就是抖音的服务器不停止运行)

以前的进程间通信时一台机器上两个进程互相通信。今天就是在两台机器上,进程进行跨网络进程间通信。

应用层还会存在一个叫做进程的东西,比如这个进程是抖音APP,那么他会接受用户输入,然后把用户的数据经过协议栈,然后再经过网络交付到目标主机,交付到目标主机不是目的,在目标主机上在找到目标进程,这个时候才算是人和人通信。IP仅仅是解决了两台物理机器之间互相通信,但是我们还要考虑如何保证双方的用户之间能看得到发送和接受的数据呢?

所以我们要有客户端进程和服务端进程,就好比唐僧就是一个报文,真正让这个报文出去的人是唐太宗,这个报文千里迢迢的被路由到了目标主机,这个叫做到达了西天,但到达西天不是目的,他还要不断向上解包分用,最终找到如来佛祖(B主机的进程就叫做如来佛祖),如来佛祖提供了一套服务(要把经书给你),经书给你,你就要带着经书(这又是一个唐僧)在返回来,再到大唐长安城,到达长安城只是你到达了目的主机,你还要向上交互给目标进程,只有交付到进程的时候,此时才能够完成进程和进程的通信,这两个进程代表的就是上层的人,客户和对方主机上给我们提供抖音,美团,这样的服务。

端口号的意义 

IP地址仅仅是解决两台物理机器之间通信,可是机器与机器之间通信没太大的意义,所以我们还要有一个东西用来标识双方两个主机之间特定的进程之间的通信,所以就衍生出来一个概念,这个概念就是端口号。

当一个报文送到了主机B上,那么主机B上的服务,可能不止跑了一个服务进程,同样当数据拿回来的时候,主机A上也可能跑了不止一个客户端。好比你的手机上,正看抖音,抖音切走后又看美团,头条...所以你的主机上可能存在大量的进程。存在多个进程就有一个问题,ip地址区分了两个主机之间不会找错。如何保证双方在通信的时候彼此之间访问的是同一个进程呢?所以就出现了端口号。

端口号的作用是唯一的标识一台机器上的唯一的一个进程!不具备全网唯一,类似你的学号,你是10号,在你们班里就是唯一的,但是其他班也可能有10号。

ip与端口号 

ip地址+端口号就能够标识互联网中的唯一的一个进程。

整个网络中存在着几百亿对的进程进行进程间通信,但是不会乱,因为不同的进程都可以通过IP+断开标识它的唯一性。所以如果把网络看做是一个大的OS系统,所有的网络上网行为,都是在这个一个大的OS内,进行进程间通信!

ip地址+port端口号 = socket(ip地址与端口号合起来就叫做套接字)

系统中进程的pid vs port(端口号)?

它俩的关系就像身份证号(pid)与学号(端口号),为什么学校不用身份证号标识你的唯一性?

如果你就想用身份证号标识你的唯一性,这样也是可以的。但是我们解决问题肯定是有好多方法的。如果学校用身份证号标识你的唯一性,但是通过身份证我们看不出来你是哪个学院的,哪个专业的,几号学生,在学校里也不便于统计历年来有多少学生,只能用来作唯一性区分,而不适合做管理。如果我们用身份证标识学生,学校的系统就只能用身份证号来编写系统,代码必须按照身份证的格式去写,万一有一天国家不用身份证,此时学校曾今写好的东西就受到了国家的影响。我们用学号最大的好处在于,不管外部环境如何变化,国家如何调整,始终不会影响到学校内部。根本原因就在于解耦。进程有pid,万一以前的pid是数字,后来pid变了,OS对进程的标识方法一旦改变,网络就要跟着变,这个的影响很大,所以我们自己定义一套port的策略。 

ip vs 端口

ip和端口是互相促进的作用,所有的网络行为都是进程间通信,我上网刷淘宝,其实是我拿着我手机的淘宝APP,在请求淘宝的网页数据给我,其实就是我的一个进程在拉取其他进程的数据,进程间通信。我与我的好友聊天,实际上是通过QQ这个进程,QQ这个进程把我的数据获取到然后转发到他的平台,然后在转发到我好友的QQ上,所以本质上进程在这种网络应用场景中,所有的APP充当的是人的代理人,代表的是一个个的客户,互联网最大的特定是连接,本质是已进程为代表,让我们所有的人和特定的人群,特定的服务,进行数据层面的交互,所以互联网的本质就是进程间通信。一个通信的进程一定有一个对应的具体设备,所以要进行通信的本质,1.找到目标主机2.在找到该主机上的服务(进程)。所以互联网世界,是一个进程间通信的世界!

互联网世界,是一个进程间通信的世界!我们聊QQ的时候,实际上是人和人在聊天,本质上还是进程间通信,我控制一个进程,你控制一个进程,我们俩通过这个进程聊天。我在使用搜索引擎的时候是一个人和一个搜索服务在做进程间通信。我在美团下单,实际上是我与吃喝玩乐的服务打交道,将来我们物联网,嵌入式,这样的世界,本质上也一定是进程间通信,不同的设备之间一定有一个进程是用来代表某个设备的,比如我的冰箱里有个进程就代表的是我的冰箱,电视里有个进程就代表电视,电视和冰箱想互相通信,就代表设备和设备通信。进程间通信是互联网的本质,但是用进程通信就是不同形态下的互联网世界。万物互联的本质就是让所有的设备都具有一个进程,而且这个进程可以代表某个设备,你家的冰箱,电视...都具有一个进程,就代表着我们的进程就可以帮我们收集各种设备的信息,甚至做各种智能化的判断,然后和其他设备甚至可以通信,最终以一个进程和人通信,给人提供服务的这样一种方案。所有的事物抽象成一个进程之后,那么这个设备必须有计算能力,就得有芯片了,我们的世界背后的硬件支持,就需要有大量的芯片和科技的进步。

进程具有独立性,进程间通信的前提工作:先得让不同的进程,看到同一份资源。今天在网络这里看到的同一份资源叫做网络。

端口号是用来标定进程的唯一性的,但是并不意味着一个进程必须得有端口号,好比每个人都必须有身份证,但是并不一定具备你们学校的学号。

一个进程可以关联多个端口号吗?一个端口号可以关联多个进程吗?

  • 可以,一个端口号标识一个进程,但是不排斥多个端口号关联同一个进程,实际中,一个进程就绑定一个端口号。
  • 不可以,端口号本身就是区分唯一性的,如果一个端口号被多个进行绑定或者关联了就不能区分唯一性了。

理解源端口号和目的端口号

传输层协议 (TCP UDP) 的数据段中有两个端口号 , 分别叫做源端口号和目的端口号 . 就是在描述 " 数据是谁发的 , 要发给谁";

认识TCP协议 

在应用层之下是传输层,传输层有两个协议。应用层是在用户层实现的,下面的一批层都是在内核或驱动程序完成的,我们不关心驱动程序,内核就是在OS内完成的。如果我们基于FTP(文件传输协议,在应用层)向上去写,我们就能写很多的服务,我们今天相当于自己造应用层,我们将来访问的一定是OS提供的接口,OS提供的接口离应用层最近的就是传输层,一般而言写常规套接字的时候,我们用的接口就是传输层的接口。换句话就是将来我们选择TCP还是UDP和对方通信。

此处我们先对 TCP(Transmission Control Protocol 传输控制协议 ) 有一个直观的认识 ; 后面我们再详细讨论 TCP 的一些细节问题.
  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

认识UDP协议

此处我们也是对 UDP(User Datagram Protocol 用户数据报协议 ) 有一个直观的认识 ; 后面再详细讨论 .
  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

简单理解TCP和UDP 

类比生活:我是一个辅导员,我周围有两个同学,一个张三,一个李四,我经常让他俩去帮我派发一些文件。张三特别靠谱,他在发送数据的时候要跟我做深度沟通,报文发的时候如果丢了怎么办,丢了后我应该找谁...跟我沟通完,然后再把这个文件转发出去。如果中间这个文件出现问题了,回来在向我要一份。这叫做TCP。

对于李四,我让他送报告,李四立马答应下来,然后李四直接就把报告拿走,我不知道他有没有送到,反正他是会给我送的,但具体什么时候送,我都不知道。不像张三,送的时候会给我打电话报告进度。李四就不跟我沟通。这就是UDP。 

作为人我们很容易给一个客观存在,打上自己的判断。TCP和UDP是两种特性的协议,TCP叫做可靠传输,UDP叫做不可靠,我们很容易认为TCP更好,这倒不一定。他俩没有谁好谁坏,只是有谁更合适。TCP可靠,就意味着它要花更多的资源,维护可靠性。UDP不可靠,就意味着它一定更简单。比如:直播的通信协议就可以选择UDP,当然银行转账必须得用TCP。一定结合应用场景。

非用TCP不可的,一定用TCP。处于成本考虑,如果不用TCP也是可以的,那就用UDP。总之,除非有明显的优点要用UDP,否则我们一律TCP。

网络字节序

我们的机器是有大端机器和小端机器的,实际上在网络中发数据的时候,比如我是大端机,你是小端机,我在发送数据的时候,永远按照内存的低地址数开始发,先发低地址数据再发高地址数据。大小端本身是一种地址和数据的对应关系,在大端机的情况下,不管是什么机器永远先发的是低地址的数据,如果是大端机,低地址上放的是高位的数据,小端机放的是低位的数据。我们两个主机大小端不一样,我的数据发给你,你再接收,整合成一个完整的数据,我们两个人看待这个数据的方式是相反的。比如,我以小端发送,你以大端接收,最终我们的数据就是反的。机器本身有大小端机器的差别(实际以小端居多),大小端不一样,就有可能出现数据相反的情况。

我们已经知道 , 内存中的多字节数据相对于内存地址有大端和小端之分 , 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分 . 那么如何定义网络数据流的地址呢 ?
  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;(数据没有被保存到内存里是没有地址的,数据只有高权值和低权值,内存地址从低到高发出,但是内存地址对应字节里面放的数据是低的还是高的完全取决于大小端)
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

大小端的概念

数据是有高权值位和低权值位的差别的,比如12345,1就是高权值位,5就是低权值位。空间内存是有高低地址之分的,所以数据存的本质就是按照字节为单位,把哪些字节放在哪些地址上,就如同在一间教室里面,我们给每张桌子都编上号,在做的同学按身高排好队,小个子坐在小编号的座位上,我们称之为小端。反之,大个子坐前面就叫做大端。  

为使网络程序具有可移植性 , 使同样的 C 代码在大端和小端计算机上编译后都能正常运行 , 可以调用以下库函数做网络字节序和主机字节序的转换。
  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

比如:互联网行为是进程间通信,进程间通信本质是一个ip和端口(套接字)和另一对ip和端口通信,当我俩通信时,通信数据和ip+端口号都是要从一个主机发送到另外一个主机上的。这样的代码可能以后我们要网络转主机,主机转网络,这样的变化。

socket编程

什么是socket 

socket的引入

为了更方便地开发网络应用程序,美国伯克利大学在UNIX上推出了一种应用程序访问通信协议的操作系统调用接字(Socket)。 Socket的出现,使得程序员可以很方便地访问 TCPIP,从而开发各种网络应用程序。后来套接字被引进到 Windows等操作系统,成为开发网络应用程序的有效工具。

套接字存在于通信区域,通信区域也被称为地址族,主要用于将通过套接字通信的进程的公有特性综合在一起。套接字通常只与同一区域的套接字交换数据。Windows Socket只支持一个通信区域——AF_INET国际网区域,使用网际协议族通信的进程使用该域 

socket的简介

socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口

socket的表示方法

套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的lP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。例如:如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23) 

socket 常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address,
 socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
 socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
 socklen_t addrlen);

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,IPv4IPv6,以及UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同。

网络通信的标准方式有很多种:

基于ip的网络通信AF_IENT(网络套接字),原始套接字,域间套接字...实际上就是一套套编程接口,如果这几种编程接口是三套体系,我们就应该有三套接口,学习就得分开学。但是网络设计者,不想这么干,至少应该把这些套接字的系统结构统一化。我想让这些不同种类的通信方式将来使用同一套接口完成不同种类的通信方式。所以就有了以下这一套。要使用不同的通信种类,只需要传入不同的参数就可以了。为了支持不同形态的套接字接口,我们就设计出sockaddr结构,它就是一种通用结构。

关于接口通用性的设计

域间套接和原始套接我们不讨论,因为原始套接本来就是一些绕过TCP/UDP去使用IP的这样一种接口,域间套接是与管道有些重叠的,但是它是有不同种类的套接字的。

我们以我们所用的AF_INET这样的使用ip进行网络通信的套接字和我们的AF_UNIX用来进行域间套接的这种套接字做对比。就好比我使用同一种接口实现两种不同的套接字。

网络通信用的是struct sockaddr_in这样的数据通信格式,它里面包含了三个字段,1个是16位类型,代表这个结构体的通信类型,我们用的是AF_INET,也叫做协议家族,再下来16位端口号,再下来32位ip地址。端口号和ip合起来就叫做套接字。我们一般端口号16位,ip地址32位。

域间套接需要的是16位的地址类型,AF_UNIX和一个108字节路径(其实和命名管道一样),这个套接字通常用来本主机内进行通信。

这个struct  sockaddr_in 和struct sockaddr_un 这两种类型是不一样的,如果这两种结构体,必须在特定的通信场景中必须被用,这里就有一个问题,我们如果设计接口,我们就得分别给这两种结构体设计一套接口。但是网络设计者,不想这么干,他就设计了struct sockaddr这样的结构,这个结构只有16位地址类型,所有的接口都传入的是struct sockaddr。当我们在传入 sockaddr_in 和struct sockaddr_un时做强转即可。在以上三个结构体内部我们对16位地址多判断,如果是AF_INET,我就认为传入的是sockaddr_in...

我们在所有接口设计上用的就是struct sockaddr这种接口,有点像struct sockaddr是一个父类,sockaddr_in 和struct sockaddr_un这是两个子类,我们用父类指针具体指向哪个对象,它最终就使用的是哪个对象内部的属性。

为什么要设计这种结构呢?

C++上我们可以用继承实现多态,在C语言上我们可以使用void*,设计这样的接口,为什么不使用void*呢?void*是可行的,void*接口一旦设计好了,我们仍然是做一个强转,直接把前16位拿出来判断是AF_INET的值,还是AF_UNIX的值,然后决定传进来的这段空间是什么类型的值。不用它的原因是因为在这套网络接口被提出来的时候,C语言的标准当时还不支持void*。现在很多的网络服务器底层都用的是这一套接口,突然把他改为void*的话很有可能出现一些问题,而且这种结构体也没啥坏处,所以就一直留到了现在。

简单的UDP网络程序

功能介绍:实现一个简单的通信,客户端发送,你好。服务端收到,你好这个信息,收到后再把信息写回给客户端,然后客户端再显示回来。

服务端(server)的编写 

创建套接字

第一个参数

domain表示域(指定协议的地址系列,该地址族确定将创建哪种类型的套接字),说明你的套接字是什么种类,这个种类指的是刚刚的sockaddr_in 或struct sockaddr_un。代表OS需要给你提供哪种通信服务,我们需要AF_INET

第二个参数

type代表的是套接字类型, 代表你想要哪种套接字

SOCK_STREAM:表示流式套接,提供的是一种按序的,可靠的,双向的基于链接的字节流服务,这种其实就是TCP。

SOCK_DGRAM:表示用户数据报,就代表的是UDP。

其余的我们用的很少。

第三个参数

protocol代表的是你这个套接字想采用的协议类型,在TCP和UDP这里,这个参数全部设为0,我们只需要告诉socket,哪一个domain,哪一个type,其中protocol这个不用指明也就明确了,所以它设为0.

socket的返回值

成功:返回文件描述符,错误:返回-1。

实际上当我们创建套接字,它以文件方式把我们的网卡设备打开,打开之后,对应执行socket函数的进程就相当于打开了一个文件,最终就变成了进程和文件的关系,一个进程可以打开多个文件,进程和文件是通过一个文件描述符表的东西关联的,每个文件要被OS管理,在底层都叫做struct_file。我们的进程底层有个对应的指针数组,指向一个个打开的文件,数组的下标称之为文件描述符。

eg:因为打开文件描述符,所以输出的值一定是3

作为一个服务器,要不要让客户知道对应服务器的地址(ip+端口)?

答案是必须的,如果别人不知道,人家也就不会访问。比如,百度,www.baidu.com是它的域名,它的域名一定对应有它的ip地址

我们访问域名是可以找到百度的

同样我们使用解析出来的百度ip地址也是可以访问百度的

说明很多互联网公司的服务,必须把服务所在的ip地址告诉你,端口号不用知道,因为你用的是浏览器,比如你直接复制下百度的ip地址,粘贴下来是这个东西http。

所以默认这些应用的端口,浏览器会自己选择端口号。实际上网站型的服务都得被你知道,这也就是为什么你每次访问百度域名,你就可以找到百度,就是因为你的浏览器在帮你根据百度的ip和端口号发起套接字通信,让你看到对应的内容。

有时候我们使用百度APP或者抖音APP就可以直接用了,从来就不需要输入抖音的域名,那是因为这些APP是对应互联网公司提供的,它的APP里面一定有配置文件或者在自己的代码里就写好了自己的服务器地址在哪里,你也就不用输入了。

换言之,无论是让用户直接输入还是间接输入,服务器的socket信息(ip+端口),必须得被客户知道!一个公司想要扩大规模,做市场宣传,在互联网的角度就是宣传域名,让大家知道我是干啥的。一般服务器的port,必须是众所周知的(不仅仅是被人,也可以被各种软件,APP,浏览器等),而且轻易不能被改变!!!一个服务器一旦起来了,端口号基本是不变的,而且尽量要让所有人都知道。 

绑定接口

第一个参数:sockfd,就是你刚刚创建好的文件描述符(你刚创建好的套接字)

第二个参数:需要用户指定服务器的相关socket信息

第三个参数:传入结构体的大小 

返回值:

成功返回0,失败返回-1 

sockaddr_in 

bind的第二个参数,我们直接用这个sockaddr_in结构,待会传参的时候强转一下即可

ps:图1是使用这个结构体对应的头文件

sockaddr_in是描述网络套接字地址的一个结构

我们可以具体看下sockaddr_in里面是什么

我们将sockaddr_in里面的源码简化一下

struct sockaddr_in
{
    short sin_family;           //协议族Address family
    unsigned short sin_port;    //16位TCP/UDP端口号
    struct in_addr sin_addr;    //32位IP地址
    unsigned char sin_zero[8];  //没有实际意义,只是为了跟SOCKADDR结构在内存中对齐
};
//该结构体中提到另一结构体in_addr定义如下,它用来存放32位IP地址
struct in_addr
{
    in_addr_t s_addr;           //32位IPv4地址
};

填充sockaddr_in 

现在我们要做的就是将sockaddr_in这个结构体填充好

前两个字段的填充

此处的端口号是我们计算机上的变量,是属于主机序列,因为这个端口号无论是服务器端还是客户端,将来他俩通信的时候,端口和ip是要互相交换的,所以这个端口要进行主机序列转网络序列 

第三个字段填充(假设这个42.2.2.2是云服务器ip)  

第三个字段是个整数,我们的ip通常是点分十进制,是字符串风格。每个区域的取值范围是[0-255].[0-255].[0-255].[0-255]。理论上4个字节就足以在网络里通信了。

在这里我们需要做两件事情:

a.需要将人能识别的点分十进制,字符串风格的ip地址,转化成为4字节整数ip

b.也要考虑大小端

这两个工作自己做是可以的,但是不建议,自己做容易出问题。系统给我们提供了接口帮助我们干这件事 inet_addr 接口

in_addr_t inet_addr(const char *cp); 就能完成上面ab两个工作,参数就是一个字符串,返回值就是in_addr_t ,就是一个32位的数。

所以我直接调用inet_addr 就可以了。

但是,云服务器,不允许用户直接bind公网ip,如果你绑定了,那么这个服务是用不了的,无法去访问这个云服务器的ip;如果你自己有一个虚拟机,那么你绑定你虚拟机这个ip地址是没有问题的;包括你自己在windows下写也是一样的。另外,我们实际正常编写的时候,我们也不会指明ip

正确写法:

转到定义,发现就是0

 

INADDR_ANY:如果你bind的是确定的IP(主机),意味着只有发到该ip主机上面的数据才会交给你的网络进程,但是,一般服务器可能有多张网卡,配置多个ip,我们需要的不是某个ip上面的数据,我们需要的是,所有发送到该主机,发送到该端口的数据!好比:有的云服务器配置特别高,人家有4个ip,如果我接下来只绑定一个,意味着客户端向主机发消息时我只能从一个ip上把数据拿上,但如果我指明INADDR_ANY,意味着我不关心这个数据是从哪个ip上来的,只要它访问的是我这个端口,都要把数据给我。

如果你想发给一个特定的ip,你只要bind特定的ip就可,只要是发送到这个主机特定端口的就必须使用INADDR_ANY。

可以手动的让socket绑定多个ip吗?

目前的接口是不可以的,如果bind的话,端口就可能出现重复的问题。

进行绑定

提供服务

既然是个服务器那么全体都应该在跑。然后就进行读数据,套接字不是文件描述符吗,与直接读文件有差别么?

有差别,如果你是UDP读,是不能以文件的方式直接去读的,我们需要专门针对UDP进行读取,我们的接口是recvform

recvfrom

第一个参数:代表从哪个套接字读

第二,三个参数:代表你自己提供的缓冲区和缓冲区大小

第四个参数:代表读的方式,默认为0就可以

第五,六个参数:严格意义上是输入输出型参数。输入:你要提供一段空间。输出:表明是谁给你发的数据。UDP服务一起来,别人就可以直接给它发消息,所以服务器就先收消息,服务器一旦收消息,对我们来讲,你将来把数据做处理,处理完之后你要给别人响应回去,响应回去的时候你需要知道是谁访问的你,这两个参数表明的是和你通信的客户端的套接字信息。

服务器一旦起来之后,别人是知道服务器的,别人给服务器发消息,服务器一旦收到这个消息,如何把对消息处理的结果返回客户端呢?所以我们需要客户端的信息。 

返回值:代表你读取了多少个字节。-1就是返回失败。

sendto

服务器读到了这个buffer,但我现在想把这个buffer返回去,用到的接口叫做sendto

第一个参数:代表创建好的套接字

第二,三个参数:代表缓冲区(你要发什么数据)和你要发送数据的长度

第四个参数:发送方式

第五,六个参数:你要sendto,就得告诉我你要向谁发,dest_addr就是对端的套接字信息,addrlen就是套接字的长度。

此时我们将相当于把数据又扔了回去。

我们现在的代码就是服务器不断的去读取数据,读到之后把数据打印出来,然后任何一个人只要给服务器发消息,我给任何人返回的一个消息就都是一个hello。通过peer知道发消息的人是谁,然后服务器在把消息返回去。

客户端(client)的编写

创建套接字

使用服务

客户端需要显示的bind吗?

a.首先,客户端必须要有ip和port,因为这是客户端的套接字,套接字本质上是需要使用一个服务端套接字和一个客户端套接字通信,所以客户端必须是要有的。

b.但是,客户端不需要显示的bind!因为,一旦显示的bind,就必须明确,client要和哪一个port关联。

client指明的端口号,在client一定会有吗?

意思就是说客户端绑定的端口在客户端就一定存在吗?比如客户端想绑定11223344。但你要绑定这个端口号的时候是有可能这个端口号被其他客户端所绑定。

服务器也会存在这样的问题啊?

但是,这两个不一样,一般对于服务器来讲,公司内部不是说你想写服务对外提供,就写对外提供的,公司是有严格的产品规划的,一个对外端口是公司的资源,公司哪些端口给哪些服务公司内部是有自己的规划的,也就是服务端的端口不会和其他服务冲突的。有些端口公司是不会让你用的。所以在公司层面上,服务器的端口是严格被在业务层严格管理起来的。

客户端不一样,有可能你在绑定这个端口的时候,其他的客户端也在跑,而且客户端很多,又没人管它,所以你要绑定一个明确的端口,比如11223344,有可能这个端口放好被浏览器,抖音,微信这样的客户端使用,那么你一旦明确的绑定了和哪个端口关联,一旦该端口被关联,那么你的客户端就不可能启动成功了,就会影响客户端。所以客户端指明端口号,在客户端是有可能被占用,被占用导致客户端无法使用。server要的是port必须明确,而且不变,但client只要有就行!没有人访问客户端,是客户端访问别人。所以端口号这个东西,在客户端不需要绑定。一般是由OS自动给客户端bind().就是client正常发送数据的时候,OS就会自动给你bind,采用的是随机端口的方式!        

客户端第二步要做的就是使用服务

使用服务,完成收发数据即可

 通过命令行参数指明应该发给谁 

通过sendto发送消息

  填充一下server 

通过recvfrom接受服务端发回来的消息

程序测试 

启动服务端

启动客户端(如果没开发端口,那么用自己的服务器ip是无法进行通信的)

如果你编写的udp无法通信,云服务器开放服务,首先需要开放端口,默认的云平台是没有开放特定的端口的,所以需要所有者在网页后端找安全组,开放端口的方式。  

我们如果还没有开放端口,我们就可以使用127.0.0.1这个ip(本地环回去测试)

我们可以看到客户端发什么消息,服务端就显示什么消息,而且每次客户端发完消息以后,服务端回复hello。

接下来我们就可以把客服端发布出去。ps:发布客户端的时候,注意要把客户端静态编译,这样就不依赖于任何的第三方库了。

如果绑定云服务器的私网ip和公网ip分别会怎么样?

之前,我们一直强调不能绑定云服务器的公网ip,并且这个地址最好绑定INADDR_ANY。

接下来我们通过实验进行验证。

博主云服务器的ip信息

绑定公网ip

我们仅仅将INADDR_ANY替换成云服务器的公网ip,其余啥也不变

执行结果:

我们发现服务压根是完全运行不起来的,也就证明不能绑定公网ip。

绑定私网ip

我们仅仅将INADDR_ANY替换成云服务器的私网ip,其余啥也不变。

执行结果:

我们发现也是可以正常去通信的。

综上:我们就可以知道绑定云服务器的公网是完全不行的。绑定云服务器的私网ip是可以的,但是我们一般不绑定云服务器的私网ip,而是绑定INADDR_ANY (原因之前提到过)。

完整代码

udp_server.cc

#include<iostream>
#include<string>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

const uint16_t port = 8080; //假设我的端口号是8080
int main()
{
    // 1.创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0); 
    if(sock<0)
    {
        std::cout << "socket create error: " << errno << std::endl;
        return 1;
    }
    
    //std::cout << "sock: " << sock << std::endl;  //一定输出3

    // 2.给服务器绑定端口和ip(特殊处理)
    struct sockaddr_in local;   //这是OS提供的类型,这个变量定义出来时在main函数的栈区的,所以我们先填充一下
    local.sin_family = AF_INET;
    local.sin_port = htons(port); //此处的端口号是我们计算机上的变量,是主机序列

    // local.sin_addr.s_addr = "42.2.2.2"; //点分十进制,字符串风格[0-255].[0-255].[0-255].[0-255]
    // a.需要将人识别的点分十进制,字符串风格的ip地址,转化成为4字节整数ip
    // b.也要考虑大小端
    // in_addr_t inet_addr(const char *cp); 就能完成上面ab两个工作

    //local.sin_addr.s_addr = inet_addr("42.2.2.2");
    // 但是这样写有一个大坑:
    // 云服务器,不允许用户直接bind公网ip,另外,我们实际正常编写的时候,也不会指明ip


    local.sin_addr.s_addr = INADDR_ANY;
    // INADDR_ANY:如果你bind的是确定的IP(主机),意味着只有发到该ip主机上面的数据
    // 才会交给你的网络进程,但是一般服务器可能有多张网卡,配置多个ip,我们需要的不是某个ip上面的数据
    // 我们需要的是,所有发送到该主机,发送到该端口的数据!

    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0) //==0绑定成功
    {
        std::cerr << "bind error" << errno << std::endl;
        return 2;
    }

    //3.提供服务
    bool quit = false;
    #define NUM 1024
    char buffer[NUM]; //缓冲区
    
    while (!quit) //服务器都是死循环 
    {
        struct sockaddr_in peer; //表示远端
        socklen_t len = sizeof(peer); //这个peer的数据空间
        recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        //我要把客户端的信息放在这个peer里,客户端的总共长度由len指明
        
        std::cout << "#client" << buffer << std::endl; //客户端发的信息就在buffer里
        
        std::string echo_hello = "hello";
        //服务器读到了这个buffer,但我现在想把这个buffer返回去.谁给我发,我就给谁发,peer给我发的,所以
        //直接把peer传入到sendto里面。
        sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr*)&peer, len);
        //                  C的接口,所以转成C的风格
    }

    return 0;
}

udp_client.cc

#include<iostream>
#include<string>
#include<cerrno>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>

void Usage(std::string proc)
{
    std::cout << "Usage: \n\t" << proc << " server_ip  server_port" << std::endl;
}

//命令行参数  将来这样执行 ./udp_client  server_ip  server_port
//ip就是我的公网ip,端口号是8080
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 0;
    }

    //1. 创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock<0)
    {
        std::cerr << "socket create error: " << errno << std::endl;
        return 1;
    }

    // 2. 客户端需要显示的bind的吗?
    // a.首先,客户端必须要有ip和port
    // b.但是,客户端不需要显示的bind!一旦显示的bind,就必须明确,client要和哪一个port关联
    // client指明的端口号,在client一定会有吗?在client是有可能被占用,被占用导致client无法使用
    // server要的是port必须明确,而且不变,但client只要有就行(一般是由OS自动给你bind)!
    // 就是client正常发送数据的时候,OS会自动给你绑定,采用的是随机端口的方式

    //填充服务端套接字信息
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2])); // atoi将字符串转成整数,htons将主机序列转成网络序列
    server.sin_addr.s_addr = inet_addr(argv[1]); 

    //2.使用服务
    while(1) //收发数据即可
    {
        // a.你的数据从哪里来?
        std::string message;
        std::cout << "输入#";
        std::cin >> message;

        // b.你要给谁发?  我不知道,客户端也不知道,我们就可以通过命令行参数指明

        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
        //通过这个套接字,把这条message消息发送到server这个主机上,就完成了客户端发送

        //此处tmp就是一个“占位符”,今天就是客户端和服务器通信,所以这个tmp保存的内容和server保存的内容一样
        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        char buffer[1024];
        recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&tmp, &len);
        //发送完后需要进行接受,从套接字里接受,这个消息一定是服务端给我发的,但是架不住别的服务
        //也给我发消息。所以我们用一个tmp当占位符。

        std::cout << "server echo#" << buffer << std::endl;
    }

    return 0;
}

关于上述代码细节性的问题

我们默认认为通信的数据是双方在互发字符串。

我们在向文件中写入的时候要不要把\0也写入文件当中?

答案是不需要的,\0是C/C++语言层面上认为它是字符串的结尾,而我们文件部分并不遵守\0作为字符串结尾这样的标志,所以你不需要\0给我,你只需要把正常的内容给我就可以了,如果把\0给我,那么它在字符串里就是乱码。我们默认认为通信的数据是双方在互发字符串。实际上,我们在网络里通信的时候,在向套接字里写的时候,它并不认为自己发的是字符串,它认为自己发的是1024个字节这么大的数据报文,这个报文里携不携带\0我并不关心,携带了最好,不携带我也没办法。所以如果我们认为通信的数据是双方在互发字符串,那么就需要我们自己把各种缓冲区信息做一下相关处理。

对于客户端

首先我们看一下recvfrom的返回值

成功会返回接受的字节大小。失败返回-1。 

我们自己定义了一个buffer,当你收到这个buffer的时候,比如对面给我发了一个hello,它认为给我发的就是h,e,l,l,o 五个字符构成一个报文。对于这个字符你自己想把它当做字符串,就需要你自己手动添加\0.然后你自己再去打印。ps:0等价与\0。

对于服务端

 在网络通信过程中,只有报文大小,或者是字节流中字节的个数,没有C/C++字符串这样的概念(虽然我们后续可能经常会遇到类似的情况)

与之前文件一样,我们向对方发送hello这个字符串,实际上向网络发送的时候,只需要发送h,e,l,l,o。\0可以不发,因为linux下一切皆文件,网络也是文件,当你向普通文件中写字符串的时候,你是不需要把\0写入的,因为把\0写入就是乱码,所以我们在网络里\0不考虑,那么如果你自己读到的数据你想把他当做字符串,那么你自己手动+\0就完了。

端口号的问题

我们服务端的端口号是写死的,是8080,我们也想把他暴露出来、

测试

我们同样通过命令行参数显示的传入端口号。

再次运行程序,此时我们就可以看到一个端口号为8081,ip为全0的服务启动了。 

执行结果:

此时我们已经完成缓冲区的问题和绑定任意端口的问题。

基于UDP实现一个简单的xshell

思路

我们今天是网络通信,我们写的就是应用层,我们现在改写代码,让我们的代码具有特定应用的功能使用。

现在我们已经可以给客户端发送字符串了,如果我们认为客户端发送过来的是命令呢?

那么就是客户端发送一个命令,在服务端执行,执行完毕之后,再把执行结果返回客户端。客户端就可以把他自己想执行的命令在服务端跑。

服务端buffer里收到的字符串我们就可以把他当做命令

如何执行该命令呢? 

popen就是一个管道,底层会创建子进程,会执行这个command,执行完这个command后就会有结果,结果最终以文件的方式呈现让你去读文件。第二个参数就是你想以什么方式打开这个文件,读方式还是写方式。

最后你可以通过pclose把他关掉。 

返回值

底层原理就是通过fork创建子进程,然后在使用pipe实现双方通信,父进程通过文件的方式拿到结果。

我们想看到popen的执行结果,我们就可以读这个文件。

读取文件的方式有很多,我们这里用fgets去读。

fgets就是从文件当中一次按一行读取,直到读到空为止。

feof判断文件流是否被读到了结尾。

对于服务端的修改

对于客户端的修改

因为我们的命令又很多的空格,cin这样的输入默认是把空格作为分隔符的,所以我们不想这么干。我们同样调用fgets进行读取

结果展示

我们通过网络就自己实现了一个简单的xshell。实际上在网络通信中,网络通信不是目的,只是一种手段,我们是要有上层应用的。我们就是通过网络的方式让客服端向服务端发命令,服务端执行后把结果返回给我。 

完整代码

udp_server.cc

#include<iostream>
#include<string>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstdio>

//const uint16_t port = 8080; //假设我的端口号是8080
std::string Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

//./udp_server  port
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return -1;
    }

    uint16_t port = atoi(argv[1]); //atoi把其转成整数

    // 1.创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0); 
    if(sock<0)
    {
        std::cout << "socket create error: " << errno << std::endl;
        return 1;
    }
    
    //std::cout << "sock: " << sock << std::endl;  //一定输出3

    // 2.给服务器绑定端口和ip(特殊处理)
    struct sockaddr_in local;   //这是OS提供的类型,这个变量定义出来时在main函数的栈区的,所以我们先填充一下
    local.sin_family = AF_INET;
    local.sin_port = htons(port); //此处的端口号是我们计算机上的变量,是主机序列

    // local.sin_addr.s_addr = "42.2.2.2"; //点分十进制,字符串风格[0-255].[0-255].[0-255].[0-255]
    // a.需要将人识别的点分十进制,字符串风格的ip地址,转化成为4字节整数ip
    // b.也要考虑大小端
    // in_addr_t inet_addr(const char *cp); 就能完成上面ab两个工作

    //local.sin_addr.s_addr = inet_addr("42.2.2.2");
    // 但是这样写有一个大坑:
    // 云服务器,不允许用户直接bind公网ip,另外,我们实际正常编写的时候,也不会指明ip


    local.sin_addr.s_addr = INADDR_ANY;
    // INADDR_ANY:如果你bind的是确定的IP(主机),意味着只有发到该ip主机上面的数据
    // 才会交给你的网络进程,但是一般服务器可能有多张网卡,配置多个ip,我们需要的不是某个ip上面的数据
    // 我们需要的是,所有发送到该主机,发送到该端口的数据!

    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0) //==0绑定成功
    {
        std::cerr << "bind error" << errno << std::endl;
        return 2;
    }

    //3.提供服务
    bool quit = false;
    #define NUM 1024
    char buffer[NUM]; //缓冲区
    
    while (!quit) //服务器都是死循环 
    {
        struct sockaddr_in peer; //表示远端
        socklen_t len = sizeof(peer); //这个peer的数据空间

        ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if(cnt > 0)
        {
            buffer[cnt] = 0;//0=='\0' ,可以当做一个字符串命令
            FILE *fp = popen(buffer, "r");

            //我们想看到popen的执行结果,我们就可以读这个文件。
            std::string echo_hello;
            char line[1024] = {0};
            while (fgets(line, sizeof(line), fp) != NULL) //此刻在不断的读取
            {
                echo_hello += line;
            }
            // if (feof(fp)) //判断一个文件流是否被读到文件结尾
            // {
            //     //读取结果完成
            // }

            pclose(fp);

            std::cout << "#client " << buffer << std::endl; //客户端发的信息就在buffer里

            //根据用户输入构建一个新的返回字符串
            
            echo_hello += "...";

            //服务器读到了这个buffer,但我现在想把这个buffer返回去.谁给我发,我就给谁发,peer给我发的,所以
            //直接把peer传入到sendto里面。
            sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr *)&peer, len);
            //                  C的接口,所以转成C的风格
        }
        //我要把客户端的信息放在这个peer里,客户端的总共长度由len指明
        else
        {
            //读取失败,我们暂时啥也不干
        }

    }

    return 0;
}

udp_client.cc

#include<iostream>
#include<string>
#include<cerrno>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>

void Usage(std::string proc)
{
    std::cout << "Usage: \n\t" << proc << " server_ip  server_port" << std::endl;
}

//命令行参数  将来这样执行 ./udp_client  server_ip  server_port
//ip就是我的公网ip,端口号是8080
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 0;
    }

    //1. 创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock<0)
    {
        std::cerr << "socket create error: " << errno << std::endl;
        return 1;
    }

    // 2. 客户端需要显示的bind的吗?
    // a.首先,客户端必须要有ip和port
    // b.但是,客户端不需要显示的bind!一旦显示的bind,就必须明确,client要和哪一个port关联
    // client指明的端口号,在client一定会有吗?在client是有可能被占用,被占用导致client无法使用
    // server要的是port必须明确,而且不变,但client只要有就行(一般是由OS自动给你bind)!
    // 就是client正常发送数据的时候,OS会自动给你绑定,采用的是随机端口的方式

    //填充服务端套接字信息
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2])); // atoi将字符串转成整数,htons将主机序列转成网络序列
    server.sin_addr.s_addr = inet_addr(argv[1]); 

    //2.使用服务
    while(1) //收发数据即可
    {
        // a.你的数据从哪里来?
        // std::string message;
        // std::cout << "输入#";
        // std::cin >> message;
        std::cout << "Myshell $";
        char line[1024];
        fgets(line, sizeof(line), stdin);

        // b.你要给谁发?  我不知道,客户端也不知道,我们就可以通过命令行参数指明

        sendto(sock, line, strlen(line), 0, (struct sockaddr *)&server, sizeof(server));
        //通过这个套接字,把这条message消息发送到server这个主机上,就完成了客户端发送

        //此处tmp就是一个“占位符”,今天就是客户端和服务器通信,所以这个tmp保存的内容和server保存的内容一样
        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        char buffer[1024];
        
        ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&tmp, &len);
        if(cnt > 0)
        {
            //在网络通信过程中,只有报文大小,或者是字节流中字节的个数,没有C/C++字符串这样的概念(虽然我们后续可能经常会遇到类似的情况)
            buffer[cnt] = 0;
            std::cout << buffer << std::endl;
        }
        else
        {
            std::cout << "出错了" << std::endl;
        }
        //发送完后需要进行接受,从套接字里接受,这个消息一定是服务端给我发的,但是架不住别的服务
        //也给我发消息。所以我们用一个tmp当占位符。

    }

    return 0;
}

简单的TCP网络程序

我们实现和刚刚UDP一样的:实现一个简单的通信,客户端发送,你好。服务端收到,你好这个信息,收到后再把信息写回给客户端,然后客户端再显示回来。

服务端

创建套接字

第一步创建套接字

所有的语法提示都是在对应的头文件里进行搜索的。

这里要注意的就是对于TCP,我们使用的就是SOCK_STREAM。

绑定

第二步就是绑定bind,对于一个服务器毫无疑问要绑定自己的ip和端口。 

首先就需要我们填充它的结构

我们把这个结构填充完,只是第一步,因为这个local是在栈上的一个变量,这个变量在OS的视角就是在用户栈区,我们的数据设置绑定肯定是要向OS内核进行设置的,所以我们进行bind

到此为止几乎和上次的udp代码是没有任何差别的。

建立连接

第三步,因为因为tcp是面向连接的 a.在通信前,需要建立连接 b.然后才能通信

什么叫做面向连接?

说人话就是,通信的时候必须先建立连接。

eg:你的朋友想要给你发快递,只需要知道你的电话,地址,就可以给你发快递了,不需要提前给你打招呼做通信,建立连接,所以发快递的过程就类似与udp。

而其中在我们的日常通信当中,还有一种通信,我在正式给你发内容的时候,我得先给你建立连接,比如说,打电话,打qq电话,或者其他通信软件的电话,比如说我要给你带电话,打电话的时候,首先我要给你拨电话,我要是不接,不好意思,我们俩没办法通信;如果我们接了,双方才能正常通信。也就是通信之前,我们在打电话这样的情况下多了一个步骤,就叫做建立连接。udp则完全不需要建立连接,因为你是拿着客户端直接上来就发消息,直接就把报文发过来了。

建立连接,就意味着一定有人主动建立连接(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)。例如学校门口的餐厅永远是当客户需要的时候,客户直接进这个饭店里面去吃饭,只要在正常的工作时间范围之内,里面随时随地的都有服务人员和工作人员招呼你吃饭。

我们当前写的是一个server,周而复始的不间断等待客户的到来,因为客户到来的时候需要建立连接,所以我们要不断的给用户提供一个建立连接的功能。

listen

接下来我们要设置套接字是listen状态,底层含义就是允许用户现在先连我了,能不能通信时另外一回事,意思是,我听着呢,你先来连接我,本质是运行用户连接。

此刻我们要做的就是监听

成功0被返回,失败返回-1.第二个参数我们暂时先不解释,只需要设置成5即可。

 为了便于区分,我们将起初设置的套接字更名为listen_sock.

提供服务

accept

作为一个tcp_server除了创建,绑定,监听以外,还需要接受连接。

accept作用就是,通过该套接字sockfd,获取到新的链接。

tcp通信过程 

第一步,一个套接字要被创建出来,第二步,进行绑定,服务器和端口必须众所周知,必须明确式的绑定出来,第三步,监听,因为服务器是tcp的,tcp通信之间得先让客户端和我建立连接,建立连接的前提是我得先能被连接,所以我设置我的状态为listen状态,意思就是说别人可以来连我了。 别人连上我了,你怎么想把这个连接拿上呢,就通过accpet。

accept的返回值 

accept的返回值,如果成功返回一个非0的整数,也就是返回一个文件描述符!而udp通信的时候,从头到尾就是一个套接字,后面进行正常的数据通信的时候,用的也都是这一个套接字。accept获取一个新的套接字,那么曾经的那个套接字又是什么?

我们以生活中的例子做出解释:

由于这几年疫情的影响,国内的旅游也是比较凋零的,那么在一些旅游环境特别好的情况下,你去玩的时候会发现,在旅游业发达的地方,竞争也非常的激烈,俗称内卷,比如我现在有一排排的商户全都是卖吃的。

太内卷的时候,我们经常会看到,某个饭店前有个人,他的工作就是搭讪路人,我们把他叫做拉客少年张三,他是被这家饭店雇佣的,他在门口不断的进行拉客,让你去吃饭。张三就走到你跟前,和你说:"几位帅哥美女快来吧,我们饭店新换的老板,新装修的场地,就连厨子都是新的,要不要来尝尝我们的特色菜",你和你的朋友一想,这必是假的啊,就想走。然后张三就说,来嘛,来嘛,我们这新开业,男生半价,女生免费。然后你就进去了。然后张三带着你和你的朋友进去饭店,当张三到了饭店的门口,它并不进入到饭店的内部,他把门一开,然后就朝着饭店里面喊一声,“来客人了,赶紧招呼下客人”。然后从后厨里,走出一个人(名为李四),然后你和你的朋友就坐到桌子旁,李四给你们拿一个菜单,你们要吃啥,喝啥给你安排的明明白白。在你们吃饭的时候,张三又跑到路边,又去拉客了。而且每次张三成功拉客,都会在饭店门口喊一声,“来客人了,快来招呼下”。可能陆陆续续的就来了王五,赵六...这样的内部进行提供服务的服务人员。在这个饭店内,每一桌的客人,都有对应的服务人员来服务,而张三的工作性质非常特殊,就是在和其他饭店的人在卷,就是充当不断的把客人从路上拉倒饭店里面吃饭的这个角色。

张三就充当的是拉客的角色,李四,王五,赵六就是给我们内部来的各种客户提供服务。所以accept这个接口传的sockfd这个套接字参数,就是曾经我们创建的套接字。也就是绑定和监听锁所传的那个套接字。我们把这个套接字叫做监听套接字。这个监听套接字就如同拉客少年张三的工作,这个监听套接字的核心工作就类似于在饭店帮我们拉客的角色拉了人以后出来招待的服务人员,李四,王五,赵六就对应accept 的返回值。这个accpet的返回值,就称之为真正提供IO服务的fd。你点菜的时候就叫做output,你吃饭的过程就是input。这两个套接字并不冲突,它们共同构建了TCP。TCP这么复杂就是因为它是一种面向连接的服务,它就必须要你先去建立连接,你就得周而复始的先获取新连接,然后才能正常通信。在我们刚刚的例子当中,正常通信的本质就是你这批客户和李四,王五,赵六...这批文件描述符进行通信。

accept的后两个参数

当别人想连你的时候,你要知道是谁连接你的,就好比你在udp通信的时候,你想知道是谁给你发的消息。连接你的那个人的ip地址和端口号是多少呢?就可以通过accept的后两个参数获得的。这个两个参数和recvfrom的后两个参数的含义是一模一样的。

后两个参数:是输入输出型参数:1.输入时代表缓冲区 2.输出时代表对端的socket信息

new_sock < 0 ,就好比拉客的张三拉客失败了,张三不会受到任何影响,转身就去拉其他客人了。类似accept获取新连接失败了,就继续循环,继续accpet。

因为tcp是面向字节流的,就如同文件一般,可以进行正常的读写。我们可以用read,write,recv,send这样的接口去读写。

客户端

创建套接字

第一步创建套接字

绑定与连接

客户端并不需要显示的绑定,客户端根本不需要有一个固定的端口号,对于客户端只要保证客户端的唯一性就好,如果你自己绑定了,你这个客户端使用固定的端口号,其他的客户端使用随机端口号,其他端口号万一随机到了你这个端口号,你这个客户端就启动失败了。永远是客户端是连接服务器,客户端是主动,server是被动。客户端关心的就是connect。

connect 

第一个参数就是你想通过哪个套接字连,第二个参数就是服务器的地址,第三个就是地址的长度

如果连接或者绑定成功,0就被返回,说明客户端是需要绑定的,只不过不需要你指明。什么时候bind呢?

就是当你发起connect的时候。

进行正常的业务请求

测试

 当我们运行服务端,我们就可以看到有一个网络服务对应的ip地址就是全0,端口号就是8081,处于listen状态。对应服务的pid是514,程序名是tcp_server。

当我们启动客户端,我们直接可以看到连接成功,

 我们使用netstat -ntp查看

 我们就可以看到客户端,地址是127.0.0.1 端口号是45906(随机端口号)服务器的ip和port就是127.0.0.1 和8081、ESTABLISHED表示连接成功了。

在下面我们还能看到tcp_server,这是因为两个进程都在一台机器上跑,这个时候在主机上查的就是两个连接。

测试

这叫做tcp通信。

我们现在写的服务器目前是只支持给一个人提供服务。之前我们的udp没这个情况是因为udp是不断进行循环读取,没有建立连接,有数据就读,没数据就直接阻塞等待。所以udp没这个情况。

完整代码

tcp_server.cc

#include<iostream>
#include<string>
#include<cstring>
#include<cerrno>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<signal.h>
#include<sys/wait.h>
#include<pthread.h>

std::string Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

//./tcp_server 8081
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    // tcp_server
    // 1.0 创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock < 0)
    {
        std::cout << "socket error: " << errno << std::endl;
        return 2;
    }

    //2.0 bind
    struct sockaddr_in local; //需要这个local 帮助我们进行绑定
    memset(&local, 0, sizeof(local)); //对这个结构体变量进行清空
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1])); //先将字符串转换为整数,让后在将其从主机序列转换成网络系列。
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    //3.0因为tcp是面向连接的,a.在通信前,需要建立连接 b.然后才能通信
    // 一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)。
    // 我们当前写的是一个server,周而复始的不间断等待客户的到来
    // 我们要不断的给用户提供一个建立连接的功能
    //
    // 设置套接字是Listen状态,本质是允许用户连接
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        std::cerr << "listen error" << errno << std::endl;
        return 4;
    }


    //对外提供服务
    for (;;)
    {
        struct sockaddr_in peer; //获取连接我的人的相关ip和端口号
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
        if (new_sock < 0)
        {
            continue;
        }

        uint16_t cli_port = ntohs(peer.sin_port); //我想知道是谁连接的我
        //客户端发给我的sin_port一定是经过对应的网络传送的,所以在peer这个结构体里面保存的一定是
        //网络字节序,所以需要网络转成主机

        //接下来就是获取ip。我们这次要的ip不能是四字节的ip,因为4字节的ip便于传输,但是不便于打印
        //我们要的就是点分十进制的字符串风格的ip地址
        std::string cli_ip = inet_ntoa(peer.sin_addr); // inet_ntoa除了可以把网络序列转成主机序列,然后还要
        //把四字节ip转换成字符串风格的ip

        std::cout << "get a new link ->:[" << cli_ip << ":" << cli_port << "]#" << new_sock << std::endl;
        
        // // version 1:单进程版本,没人使用!
        // //提供服务,我们是一个死循环
        // //ServiceIO(new_sock);
        while (true)
        {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t s = read(new_sock, buffer, sizeof(buffer) - 1);
            // listen_socket的使命在获取新连接后就已经完成了,所以我们用new_scok来读,把内容读到buffer中

            if (s > 0)
            {
                buffer[s] = 0; //将获取的内容当成字符串
                std::cout << "client# " << buffer << std::endl;

                std::string echo_string = ">>>server<<<, ";
                echo_string += buffer;
                write(new_sock, echo_string.c_str(), echo_string.size());
                //把读到的内容写回到客户端
            }
            else if (s == 0) //说明对端把连接关了
            {
                std::cout << "client quit..." << std::endl;
                break;
            }
            else
            {
                std::cerr << "read error" << std::endl;
                break;
            }
        }
    }

    return 0;
}

tcp_client.cc

#include<iostream>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<cstdlib>
#include<strings.h>

std::string Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " server_ip servere_port" << std::endl;
}

// ./tcp_client server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    std::string svr_ip = argv[1];
    uint16_t svr_port = atoi(argv[2]);

    //1.创建套接字socket
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }

    //2.bind ,client 无需显示的绑定,client连接服务器,
    struct sockaddr_in server;
    bzero(&server, sizeof(server)); //bzero等价于memset,把空间全部清0 但是不推荐
    server.sin_family = AF_INET;

    //该函数做两件事情
    //1.将点分十进制的字符串风格的ip转化为4字节ip
    //2.将四字节由主机序列转化为网络序列
    server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
    server.sin_port = htons(svr_port);

    //发起连接
    if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
    {
        std::cout << "connect server failed !" << std::endl;
        return 3;
    }

    std::cout << "connect success!" << std::endl;

    //进行正常的业务请求
    while(true)
    {
        std::cout << "Please Enter# ";
        char buffer[1024];
        fgets(buffer, sizeof(buffer) - 1, stdin);

        write(sock, buffer, strlen(buffer)); //该套接字已经连接服务器成功了,所以直接向该套接字写入。
        //写入的数据在buffer里,buffer不用+1,有多少有效字符直接发过去就可以了

        ssize_t s = read(sock, buffer, sizeof(buffer) - 1); //通过read拿到对应数据的返回
        //从套接字里读,读到buffer里面
        if (s > 0)
        {
            buffer[s] = 0; //我把它当做字符串
            std::cout << "server echo# " << buffer << std::endl; //把服务端发给我的数据读取出来
        }
    }

    return 0;
}

针对上述TCP通信的扩展 

存在的问题

我们的服务器是目前一个单进程的服务器,意味着服务器一旦进入提供服务的死循环,服务器就给一个人服务了,如果还有人连接我的时候,你可能会连接成功,但是实际上无法进入这个循环。因为当前我的用户层进程正在给上一个人提供服务。当某个用户退出,才会跳出死循环,给下一个用户提供服务。

eg: 两台主机同时连接,虽然都能连接成功,但是只给先连接的那一台提供服务。

 第一个客户端,退出以后,才能为第二个客户端提供服务

解决方案一

所以我们可以利用子进程解决这个问题。

我们先把提供服务的函数单独拎出来

对于子进程提供完服务,我们的父进程必须得等,等待就有阻塞等待和非阻塞等待。

阻塞等待:那么子进程必须把服务提供完,我们进行对应的阻塞等待,那这个的本质还是一次等待一个 ,一次给一个人提供服务这是不合理的

非阻塞等待:这也不好,因为不断有连接到来,你得把所有的子进程管理起来,成本太高了

所以我们通过一个信号,在linux中,父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源。实现我们的诉求。

目前的逻辑就是来一个新的连接,我就创建一个子进程,让子进程去给跟你的客户端进行通信,通信完了,子进程退出,父进程不管,因为此时我的父进程已经忽略了子进程,忽略之后,子进程退出的时候,OS会自动释放子进程的资源,进行回收。

测试结果:

我们发现,最开始连接的文件描述符是从4,5,6开始的,当退出去重新连接,文件描述符就变成了7,8,9...这个问题说明,当你每一次连接,服务器就会启动一个新的套接字来让我们去连,可是你自己在写server的时候,是没有进行关闭对应的连接的。我们的服务器给客户端提供服务是以子进程的方式提供服务的,子进程和父进程之间文件描述符的关系也没有考虑。 

优化1.0

首先我们可以让服务端打印出所连接客户端的相关信息(ip和port)

ps:小问题 

在之前C/C++中,我们常用char,int 等类型,但是在网络程序中,我们往往比较喜欢使用uint16_t这样的类型,这种类型仅仅表示的是字节的含义,如图就表示占两个字节,16个比特位。主要是因为保证代码的兼容性和可移植性,在不同的平台当中,char,int,double等这样类型的大小可能会不一样,但uint16_t 这样的大小类型是确定的,不会随着代码在不同平台下进行编译,而导致代码当中开辟的字节数发生变化。

关于文件描述符

在服务端,listen_sock的fd是3,new_sock的fd是4,那么当fork创建子进程的时候,子进程会不会 继承父进程曾经打开的3或者4这样的fd呢?

答案是会的,在创建子进程之前,3和4肯定被打开了,当子进程对客户提供服务的,是可以看到父进程打开的3,4号文件描述符的,也可以通过3,4做一些操作,但是3号被子进程看到了,就存在一些问题,万一3号被子进程误读了呢?3号可是服务器获取新连接的非常重要的文件描述符,如果误读了就出问题了,所以我们一定有个原则,无论是父子进程中的哪一个,我们都强烈建议关闭掉不需要的fd。

当服务完成之后,就关闭套接字,为什么必须关呢?

多进程环境下,一个进程所用的文件描述符,即使在多,它也是有上限的,文件描述符天然就是一种资源,所以如果你不关闭文件描述符导致可用的文件描述符资源变得越来越少,导致的现象就是越往后连接文件描述符越大,说白了就是你的进程把fd打开后没有进行关闭,所以这就叫做fd泄露。如果不关闭不需要的fd,会造成fd泄露(意思就是这些文件描述符你再也用不了了,因为本身就没有被关掉)。

ps:执行到父进程这里以后 ,说明fork已经返回了,说明子进程已经存在了,说明子进程已经把父进程刚刚创建的new_sock继承下去了,子进程已经可以用了。父进程只需要把刚刚accept的new_sock关闭,因为是子进程需要new_sock进程通信的,父进程不需要。

优化1.0进行测试:

(我们通过再执行客户端后面+&就能实现同一台主机上连接多个客户端)

我们可以看到,每一个连接的人,它的fd都是4,因为所有连我的人,它一连接上,子进程就给他提供服务了,占的是子进程的文件描述符,然后父进程会把刚刚打开的文件描述符关掉,但是不影响子进程,子进程给你提供服务去了。下次再连接的人它还是4,除非一次连接了大量的客户端,这个时候才会出现4,5,6这样的情况。这样的话就能把父进程的文件描述符一直控制在有效范围之内。

ps:这样连接以后记得进行退出

完整代码:

#include<iostream>
#include<string>
#include<cstring>
#include<cerrno>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<signal.h>
#include<sys/wait.h>
#include<pthread.h>

std::string Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

void ServiceIO(int new_sock)
{
    while (true)
    {
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));
        ssize_t s = read(new_sock, buffer, sizeof(buffer) - 1);
        // listen_socket的使命在获取新连接后就已经完成了,所以我们用new_scok来读,把内容读到buffer中

        if (s > 0)
        {
            buffer[s] = 0; //将获取的内容当成字符串
            std::cout << "client# " << buffer << std::endl;

            std::string echo_string = ">>>server<<<, ";
            echo_string += buffer;
            write(new_sock, echo_string.c_str(), echo_string.size());
            //把读到的内容写回到客户端
        }
        else if (s == 0) //说明对端把连接关了
        {
            std::cout << "client quit..." << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}

//./tcp_server 8081
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    // tcp_server
    // 1.0 创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock < 0)
    {
        std::cout << "socket error: " << errno << std::endl;
        return 2;
    }

    //2.0 bind
    struct sockaddr_in local; //需要这个local 帮助我们进行绑定
    memset(&local, 0, sizeof(local)); //对这个结构体变量进行清空
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1])); //先将字符串转换为整数,让后在将其从主机序列转换成网络系列。
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    //3.0因为tcp是面向连接的,a.在通信前,需要建立连接 b.然后才能通信
    // 一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)。
    // 我们当前写的是一个server,周而复始的不间断等待客户的到来
    // 我们要不断的给用户提供一个建立连接的功能
    //
    // 设置套接字是Listen状态,本质是允许用户连接
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        std::cerr << "listen error" << errno << std::endl;
        return 4;
    }

    signal(SIGCHLD, SIG_IGN); //在linux中,父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源

    //对外提供服务
    for (;;)
    {
        struct sockaddr_in peer; //获取连接我的人的相关ip和端口号
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
        if (new_sock < 0)
        {
            continue;
        }

        uint16_t cli_port = ntohs(peer.sin_port); //我想知道是谁连接的我
        //客户端发给我的sin_port一定是经过对应的网络传送的,所以在peer这个结构体里面保存的一定是
        //网络字节序,所以需要网络转成主机

        //接下来就是获取ip。我们这次要的ip不能是四字节的ip,因为4字节的ip便于传输,但是不便于打印
        //我们要的就是点分十进制的字符串风格的ip地址
        std::string cli_ip = inet_ntoa(peer.sin_addr); // inet_ntoa除了可以把网络序列转成主机序列,然后还要
        //把四字节ip转换成字符串风格的ip

        std::cout << "get a new link ->:[" << cli_ip << ":" << cli_port << "]#" << new_sock << std::endl;

        // //version 2版本 + 2.1版本
        pid_t id = fork();
        if (id < 0) //创建失败
        {
            continue;
        }
        else if (id == 0) //子进程
        {
            close(listen_sock); //子进程是会继承父进程的相关信息的,所以关闭的时候并不影响父进程
            ServiceIO(new_sock);
            close(new_sock);
            //当服务完成之后,就关闭套接字,为什么必须关呢?
            //多进程环境下,一个进程所用的文件描述符,即使在多,它也是有上限的,文件描述符天然就是一种资源
            //所以如果你不关闭文件描述符导致可用的文件描述符资源变得越来越少,导致的现象就是越往后连接文件
            //描述符越大,说白了就是你的进程把fd打开后没有进行关闭,所以这就叫做fd泄露。如果不关闭不需要的
            //fd,会造成fd泄露(意思就是这些文件描述符你再也用不了了,因为本身就没有被关掉)。


            exit(0); //让子进程直接终止
            //父进程必须得等,父进程如果阻塞等待,那么子进程必须把服务提供完,本质还是一次等待一个
            //非阻塞等待也不好,因为不断有连接到来,你得把所有的子进程管理起来,成本太高了

            //此刻就是来一个新连接,我就给你创建一个子进程,让子进程给你一个服务端进行通信,通信
            //完了,子进程退出,父进程不管,因为父进程忽略了,所以OS会自动回收子进程的资源
        }
        else  //父进程
        {
            //do nothing 父进程啥也不干,回到最开始,继续accept新连接。
            close(new_sock); //父进程并不需要这个new_sock,而且这个new_sock已经被子进程继承了,不影响子进程
        }
        //父进程不断accept新连接,子进程不断提供服务,服务完你的资源自动回收。

        
    }

    return 0;
}

优化2.0

在上述代码的基础上,再次进行如下修改

第一步,不在使用信号忽略

第二步

通过加这两行代码,就可以做到让父进程不需要等待,此时127行退出的就是子进程,向后走的进程其实是孙子进程,爷孙进程是没有等待关系的,孙子进程的爸爸已经退出了,所以这个孙子进程和我们当初的父进程没有关系。孙子进程的爸爸挂掉了,所以此时这个孙子进程是一个孤儿进程,它既然是孤儿进程那么就不应该让我的父进程去处理,而是被OS领养了,领养之后孙子进程执行,执行完后回收资源我通通不关心。父进程疯狂的进程获取新连接就可以,照顾这个孙子进程的,完全是由OS做的。

测试:

和刚才的效果一致。

当客户端再次建立好连接以后(下图左下,我们看到新建立了4个客户端连接),我们再次查看下当前服务端。

我们看到存在僵尸进程,我们现在客户端是连接着服务器的,而且客户端没退出,那么这个僵尸进程是怎么来的呢?

父进程是不需要等待孙子进程,但是这个立马退出的子进程父进程是要进行回收的,

这里进行等待的时候,会不会被阻塞呢?

不会,严格说是不会阻塞太长的时间,因为你刚创建出子进程,它直接就退出了,所以这个父进程就立马回收掉了这个子进程,它的退出结果不重要,我们也不关心。然后父进程继续获取新连接,子进程继续向后走,孙子进程提供服务。

再次测试

此时就不存在僵尸进程了。 

解决方案二

既然我们可以用进程处理一个一个的连接请求,我们当然可以使用我们的pthread库,我们可以使用线程执行业务处理。

这个线程要对外提供服务,那么主线程就要进行pthread_join(),但是一旦join了,只有join结束后得到结果才能accpet,那么主执行流获取连接和我们的新线程到来一个连接的时候,本质上就相当于大家你玩你的,我玩我的,最后导致的结果还是串行的。这样肯定不行,所以我们不想等待线程,我们就可以将这个线程进行分离,分离之后,我们的主线程就继续accept新连接了,不再关心这个线程,而新线程就跑过去处理我们的请求了。

曾经被主线程打开的fd,新线程是否能看到,是否共享?

答案是:能看到,共享。所有的线程创建出来都是共享地址空间的,文件描述符表都是大家共享的,所以你创建出来的线程就不能像多进程一样关闭listen_sock.如果关了,服务器就挂了,获取不了新连接了。只要每个线程获得新的套接字,用完之后,把他关掉就可以了,每个线程都执行这样的动作,那么文件描述符就不会被泄露 。

完整代码:

#include<iostream>
#include<string>
#include<cstring>
#include<cerrno>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<signal.h>
#include<sys/wait.h>
#include<pthread.h>

std::string Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

void ServiceIO(int new_sock)
{
    while (true)
    {
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));
        ssize_t s = read(new_sock, buffer, sizeof(buffer) - 1);
        // listen_socket的使命在获取新连接后就已经完成了,所以我们用new_scok来读,把内容读到buffer中

        if (s > 0)
        {
            buffer[s] = 0; //将获取的内容当成字符串
            std::cout << "client# " << buffer << std::endl;

            std::string echo_string = ">>>server<<<, ";
            echo_string += buffer;
            write(new_sock, echo_string.c_str(), echo_string.size());
            //把读到的内容写回到客户端
        }
        else if (s == 0) //说明对端把连接关了
        {
            std::cout << "client quit..." << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }
}

void *HandlerRequst(void *args)
{
    pthread_detach(pthread_self()); //进程线程分离,那么主线程就再也不关心这个线程了
    int sock = *(int *)args; //拿到new_sock
    delete (int *)args;

    ServiceIO(sock); //进行服务
    close(sock); //服务完成后,把文件描述符关掉
}

//./tcp_server 8081
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    // tcp_server
    // 1.0 创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock < 0)
    {
        std::cout << "socket error: " << errno << std::endl;
        return 2;
    }

    //2.0 bind
    struct sockaddr_in local; //需要这个local 帮助我们进行绑定
    memset(&local, 0, sizeof(local)); //对这个结构体变量进行清空
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1])); //先将字符串转换为整数,让后在将其从主机序列转换成网络系列。
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    //3.0因为tcp是面向连接的,a.在通信前,需要建立连接 b.然后才能通信
    // 一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)。
    // 我们当前写的是一个server,周而复始的不间断等待客户的到来
    // 我们要不断的给用户提供一个建立连接的功能
    //
    // 设置套接字是Listen状态,本质是允许用户连接
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        std::cerr << "listen error" << errno << std::endl;
        return 4;
    }

    //signal(SIGCHLD, SIG_IGN); //在linux中,父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源

    //对外提供服务
    for (;;)
    {
        struct sockaddr_in peer; //获取连接我的人的相关ip和端口号
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
        if (new_sock < 0)
        {
            continue;
        }

        uint16_t cli_port = ntohs(peer.sin_port); //我想知道是谁连接的我
        //客户端发给我的sin_port一定是经过对应的网络传送的,所以在peer这个结构体里面保存的一定是
        //网络字节序,所以需要网络转成主机

        //接下来就是获取ip。我们这次要的ip不能是四字节的ip,因为4字节的ip便于传输,但是不便于打印
        //我们要的就是点分十进制的字符串风格的ip地址
        std::string cli_ip = inet_ntoa(peer.sin_addr); // inet_ntoa除了可以把网络序列转成主机序列,然后还要
        //把四字节ip转换成字符串风格的ip

        std::cout << "get a new link ->:[" << cli_ip << ":" << cli_port << "]#" << new_sock << std::endl;

        //version 3 曾经被主线程打开的fd,新线程是否能看到,是否共享?
        pthread_t tid;
        int *pram = new int(new_sock);
        pthread_create(&tid, nullptr, HandlerRequst, pram);

    }

    return 0;
}

测试:

此次编译的时候要用到线程库

测试脚本

while :; do ps -aL | head -1 &&ps -aL | grep tcp_server; sleep 1; echo "#################################";done

客户端连接 

证明我们确实是有多个线程

当客户端退出时

再次连接,文件描述符又从4开始递增(第一个6是打印的问题)

这说明我们曾经释放文件描述符的动作是成功的,说明我们的文件描述符随着不断连接,不断的到来,我们的文件描述符是会被回收的。下次在连接的时候,文件描述符就又从4开始了。

多线程或者多进程的利弊 

无论是多进程还是多线程都是进步的一种体现,以前是服务器是单人可以使用,现在的服务器可以多人并发去访问,当然这种服务器很明显,因为随着客户量的增多,系统中的进程和线程也会越来越多, 也一定会引起服务器本身的工作压力越来越大,像今天写的这个服务器,如果想搞挂掉,我只要拿着客户端不断去连,因为你的服务本身不是短服务而是长服务,是一直在循环的给客户端提供服务,那么只有客户端退出的时候,你这个服务才会停止,我只要连上你,然后不退出,最后系统中进程或者线程越来越多,服务器扛不住了就会挂掉(你没办法获取新连接,没办法创建新的进程线程就等同于挂了)

我们口中的服务器就是部署在云服务器上的某些软件服务,你自己用公司或者企业给你提供的客户端来访问服务,所以平时刷抖音,淘宝,去打游戏的时候,它的工作方式全部是我们以上写的这种。你可以想象成当你连上王者荣耀后,就有一个新的套接字,进程或者线程给你创建出来,这个进程或者线程就代表你,替你去访问服务器上给你提供的一些资源。

解决方案三 

方案一和方案二存在的问题

但是这样写还是存在问题的。方案一和方案二,存在两个问题。

问题1:创建线程或者进程无上限,如果没有上限的话,作为一名恶意分子,我想把你的服务器攻击一下,只需要找一大批机器,这一批机器同时向你的服务器发起请求,你的服务器就有大量的进程或者线程,进程或者线程越多并不意味着你的服务器效率越高,因为以前服务是主要的时间消耗就转换成了进程切换是主要的消耗,假如我有10000个进程,一旦我被切换走,当我再想被换上来,我就得等上9000多个进程,那时间对我来讲就变的更久了,切换成本变的更高了。当进程或者线程创建没上限的话,就会导致进程或者线程过多,导致系统运行的非常非常慢,进而导致服务器无法正常向外提供服务。

问题2:当客户连接来了,我们才给客户创建进程或者线程。

我已将连上服务器了,当我连上服务器以后,你这个时候才给我进行所谓的fork或者pthread_create,这就好比你去一家食堂或者餐厅里面去吃饭,比如你要吃一个西红柿炒鸡蛋,然后服务员说,你等等,我去给你种西红柿,这样等做好以后你就饿死了。你不能把创建进程或者线程这样服务器本来应该做的事情这样的时间成本记在客户端的头上,就好比我吃西红柿炒鸡蛋,你不要把种西红柿花的时间,还有让母鸡下蛋的时间记在我的头上,让我去等,那么我的时间就白白浪费了。

方案三进程或者线程池

完整代码:

tcp_server.h

#include<iostream>
#include<string>
#include<cstring>
#include<cerrno>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<signal.h>
#include<sys/wait.h>
#include<pthread.h>
#include"thread_pool.hpp"
#include"Task.hpp"

using namespace ns_threadpool;
using namespace ns_task;
std::string Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}


//./tcp_server 8081
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }

    // tcp_server
    // 1.0 创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock < 0)
    {
        std::cout << "socket error: " << errno << std::endl;
        return 2;
    }

    //2.0 bind
    struct sockaddr_in local; //需要这个local 帮助我们进行绑定
    memset(&local, 0, sizeof(local)); //对这个结构体变量进行清空
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1])); //先将字符串转换为整数,让后在将其从主机序列转换成网络系列。
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error: " << errno << std::endl;
        return 3;
    }

    //3.0因为tcp是面向连接的,a.在通信前,需要建立连接 b.然后才能通信
    // 一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)。
    // 我们当前写的是一个server,周而复始的不间断等待客户的到来
    // 我们要不断的给用户提供一个建立连接的功能
    //
    // 设置套接字是Listen状态,本质是允许用户连接
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        std::cerr << "listen error" << errno << std::endl;
        return 4;
    }

    //signal(SIGCHLD, SIG_IGN); //在linux中,父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源

    //对外提供服务
    for (;;)
    {
        struct sockaddr_in peer; //获取连接我的人的相关ip和端口号
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
        if (new_sock < 0)
        {
            continue;
        }

        uint16_t cli_port = ntohs(peer.sin_port); //我想知道是谁连接的我
        //客户端发给我的sin_port一定是经过对应的网络传送的,所以在peer这个结构体里面保存的一定是
        //网络字节序,所以需要网络转成主机

        //接下来就是获取ip。我们这次要的ip不能是四字节的ip,因为4字节的ip便于传输,但是不便于打印
        //我们要的就是点分十进制的字符串风格的ip地址
        std::string cli_ip = inet_ntoa(peer.sin_addr); // inet_ntoa除了可以把网络序列转成主机序列,然后还要
        //把四字节ip转换成字符串风格的ip

        std::cout << "get a new link ->:[" << cli_ip << ":" << cli_port << "]#" << new_sock << std::endl;
        
        //version 4 进程或者线程池版本
        Task t(new_sock);  //1.获取新连接后,构建一个任务

        //2.将任务push到后端的线程池即可,主线程push完后,主线程继续获取连接,服务就有线程池为我们提供
        ThreadPool<Task>::GetInstance()->PushTask(t);
        
    }

    return 0;
}

thread_pool.hpp

#pragma once

#include<iostream>
#include<string>
#include<queue>
#include<unistd.h>
#include<pthread.h>

namespace ns_threadpool
{
    const int g_num = 5;

    template <class T>
    class ThreadPool
    {
    private:
        int num_;                  //这个线程池有多少个线程
        std::queue<T> task_queue_; //任务队列,是一个临界资源
        pthread_mutex_t mtx_;      //这个锁就是和任务队列匹配的
        pthread_cond_t cond_;

        static ThreadPool<T> *ins; //必须有一个类内的静态指针,获取对象的时候不能适使用ThreadPool创建对象,必须通过ins获取
    
    private:

        //既然是单例,构造函数必须实现,但是必须得私有化,构造函数私有就相当于这个类就不能构造对象了
        ThreadPool(int num=g_num)
        :num_(num)
        {
            pthread_mutex_init(&mtx_, nullptr);
            pthread_cond_init(&cond_, nullptr);
        }

        //我们不想让这个线程池发生所谓的拷贝构造或者赋值
        ThreadPool(const ThreadPool<T> &tp)=delete;  //不发生拷贝

        ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete; //不发生赋值

    public:
        ~ThreadPool()
        {
            pthread_mutex_destroy(&mtx_);
            pthread_cond_destroy(&cond_);
        }

    public:
        // Routine是类内的成员函数,每个成员函数都隐含的传入了一个this指针
        //类内中要执行线程处理逻辑,必须让线程执行静态方法。因为一个类的成员方法被改成静态,那么它是没有this指针的
        static void *Routine(void* args)
        {
            pthread_detach(pthread_self()); //分离线程一旦分离后,主线程继续向后走,新线程就执行Routine
            ThreadPool<T> *tp = (ThreadPool<T> *)args;
            while (true)
            {
                tp->Lock();
                while(tp->IsEmpty())
                {
                    //任务队列为空,线程该干什么呢?
                    //我们就应该让线程挂起
                    tp->Wait(); //就需要有一个条件变量
                }
                //该任务队列中一定有任务了
                T t;  //创建的任务
                tp->PopTask(&t);
                tp->Unlock();

                t.Run(); //执行任务自己的方法
                //在解锁之外处理任务,当你把锁释放掉以后,你当前的线程可能正在处理这个任务,
                //其他线程也可以征用锁,判断是否有任务,有任务后在进行处理。所以我们在线程池内就存在有多个线程在处理任务。
            }
        }  

        void InitThreadPool()
        {
            pthread_t tid;
            for (int i = 0; i < num_;i++)
            {
                pthread_create(&tid, nullptr, Routine, (void *)this);
            }
        }

        void PushTask(const T& in)
        {
            Lock();
            task_queue_.push(in); //放任务的时候,线程池内部有许多线程竞争式的那任务,所以需要锁
            Unlock();
            Wakeup();
        }

        void PopTask(T *out)
        {
            *out = task_queue_.front();
            task_queue_.pop();
        }

    public:
        static ThreadPool<T>  *GetInstance() 
        {
            static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //因为是静态锁,可以用宏去初始化

            if(ins ==nullptr) //双判断,减少锁的争用,提高获取单例的效率
            {
                pthread_mutex_lock(&lock);
                if (ins == nullptr) //当前单例对象还没有被创建
                {
                    ins = new ThreadPool<T>();
                    ins->InitThreadPool(); //一创建好对象,就把线程池立马初始化好
                    std::cout << "首次加载对象" << std::endl;
                }
                pthread_mutex_unlock(&lock);
            }

            return ins;
        }

        void Lock()
        {
            pthread_mutex_lock(&mtx_);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&mtx_);
        }

        bool IsEmpty()
        {
            return task_queue_.empty();
        }

        void Wait()
        {
            pthread_cond_wait(&cond_, &mtx_);
        }

        void Wakeup()
        {
            pthread_cond_signal(&cond_);
        }
    };

    template <class T> 
    ThreadPool<T>* ThreadPool<T>::ins = nullptr;  //类内的静态成员初始化必须在类外进行初始化
    //类型是线程池指针类型,访问ins带上作用域
}

Task.hpp

#pragma once

#include <iostream>
#include<cstring>
#include<unistd.h>

namespace ns_task
{
    class Task
    {
    private:
        int sock_;
    public:
        Task()
        :sock_(-1)
        {}

        Task(int sock)
        :sock_(sock)
        {}

        int Run()
        {
            //我不想进行长服务,你连接上,我只让你和服务器说一句话,要不然很容易把线程池占满
            //我们现在提供的服务就是读取,读取完后,把对应的响应返回给你,返回完毕后就退出来。
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t s = read(sock_, buffer, sizeof(buffer) - 1);
            // listen_socket的使命在获取新连接后就已经完成了,所以我们用new_scok来读,把内容读到buffer中

            if (s > 0)
            {
                buffer[s] = 0; //将获取的内容当成字符串
                std::cout << "client# " << buffer << std::endl;

                std::string echo_string = ">>>server<<<, ";
                echo_string += buffer;
                write(sock_, echo_string.c_str(), echo_string.size());
                //把读到的内容写回到客户端
            }
            else if (s == 0) //说明对端把连接关了
            {
                std::cout << "client quit..." << std::endl;
            }
            else
            {
                std::cerr << "read error" << std::endl;
            }

            close(sock_);
        }

        int operator()()
        {
            return Run();
        }

        ~Task()
        {
        }
    };
}

代码逻辑

目前的逻辑就是,首先创建套接字,然后进行绑定,然后进行监听,监听成功以后获取新连接,一旦获取一个新的套接字,我们就构建一个新的任务对象,然后就把这个任务对象,通过单例push到任务队列里去,因为单例,所以这个单例被首次调用的时候会创建对象,然后调用init把线程组给创建出来,然后返回。然后再去调用PushTask去push任务(这个任务是一个个任务队列中的任务),push进去后,新起的线程就不断的从任务队列中拿任务,如果队列为空就等,拿到任务后就调用任务的处理方法(进行通信,通信完后关闭连接)。那么这个线程就处理任务完成,迅速进入下一个循环继续处理。

测试

运行监视脚本

while :; do ps -aL | head -1 &&ps -aL | grep tcp_server; sleep 1; echo "#################################";done

因为是单例+懒汉,此刻没有人连接我,是只有一个主线程的,没有其他线程。

 当有人第一次连接我,就会给我们创建单例,然后有了一批线程

再次连接一个客户端,我们看到线程还是5个(不算主线程),总之就是不管客户端在怎么多,线程数始终是5个

我们发现,我发完hello的时候,再发就发现没有反应了,因为我们是短连接,短连接后,客户端应该是没做关闭连接的处理,有一点点的问题,但是不影响。

我们还发现,当连接的线程退出后,再次连接,文件描述符仍然是从4开始的,因为我们关闭行为,这样就不存在内存泄露的问题。

套接字总结

1.创建socket的过程,本质是打开文件,因为linux下一切皆文件,网络或者网卡这样的设备,本质上就是文件,所以调用socket的时候,我们得到的是一个文件描述符,就证明本质是打开文件。这个文件里面其实是没有包含任何网络的信息的,这里仅仅有系统相关的内容。你当初打开一个文件,就创建一个struct file 。struct file和进程通过fd关联起来。struct file有自己的inode属性和缓冲区,没有网络信息。

2.当我们做bind() 的时候,需要填充structaddr_in结构,它里面最重要的就是ip,port。ip和port填好,进行绑定,本质是将ip+port和文件信息进行关联。也就是刚打开的文件啥也没有,然后我们将ip+port信息和文件关联起来。

3.listen(),本质是设置该socket文件的状态,允许别人来连接我

4.accept(),本质是获取新连接到应用层,是以fd为代表的。

什么叫做连接?

当有很多个连接,连上我们的服务器的时候,OS中就会存在大量的连接,OS就会进行管理这些已经建立好的连接。通过先描述,在组织进程管理。所谓的“连接”,在OS层面,本质上其实就是一个描述连接的结构体。 

5. read/write,本质就是进行网络通信,但是,对于用户来讲,相当于我们在进行正常的文件读写。

6.close(fd),关闭文件,本质就是把fd和文件之间的关系去掉,如果没有人指向这个文件,我们就可以把这个文件free掉了。

a.系统层面,释放曾经申请的文件资源,连接资源

b.网络层面, 通知对方,我的连接已经关闭了!

7.connect(),本质是发起连接,在系统层面,就是构建一个请求报文发送过去;在网络层面,发起tcp连接的3次握手。当客户端connect,服务器处于listen状态,客户端就可以向我们发起一次连接请求,这个就叫做三次握手。就是客户端发起请求,底层就会自动握手3次,也就是报文交换3次,双方才认为他们的连接是建立成功的,建立成功后,connect返回,就可以正常读写了,accept返回就会得到一个新的文件描述符,正常发送数据就是读写的过程。

8.close(), client&&server ,本质在网络层面,其实就是在进行4次挥手。当我们双方调用close的时候,自动调用一个close就是一来一回,2次挥手,另外一端也调用close,就是在一来一回,4次挥手。

我们究竟在干什么?

OS给我们使用网络提供一批系统调用,eg:socket,bind,listen,accept,connect,read,write。我们实际上所做的工作是在从零开始,编写应用层!

但是在应用层中,有一批人,早就把代码写好了,也就意味着我们可以直接使用别人的应用层代码完成某件工作,但是我们今天并没有使用别人的应用层,而是编写应用层。按照我们今天所写的,我们其实是写了一个基于udp或者tcp的数据传输功能的一个应用层代码,我们没有定制任何的应用层协议,我们也没做任何相关工作,我们只是在应用层传输了一些数据,没有搭建应用逻辑。这就是我们做的工作。

TCP协议通讯流程(附)

下图是基于TCP协议的客户端/服务器程序的一般流程:

服务器初始化:

  • 调用socket, 创建文件描述符;
  • 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
  • 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
  • 调用accecpt, 并阻塞, 等待客户端连接过来;

建立连接的过程:

  • 调用socket, 创建文件描述符;
  • 调用connect, 向服务器发起连接请求;
  • connect会发出SYN段并阻塞等待服务器应答; (第一次)
  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  • 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK; (第三次)
这个建立连接的过程 , 通常称为 三次握手 ;

数据传输的过程:

  • 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
  • 服务器从accept()返回后立刻调 用read(), socket就像读管道一样, 如果没有数据到达就阻塞等待;
  • 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
  • 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
  • 客户端收到后从read()返回, 发送下一条请求,如此循环下去;

断开连接的过程:

  • 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN(第一次);
  • 此时服务器收到FIN, 会回应一个ACK, 同时read会返回0 (第二次);
  • read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN; (第三次)
  • 客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程 , 通常称为 四次挥手

在学习socket API时要注意应用程序和TCP协议层是如何交互的:

  • 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN
  • 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN

TCP UDP 对比

可靠传输 vs 不可靠传输
有连接 vs 无连接
字节流 vs 数据报

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值