一、网络的层状划分结构
我们网络的层级,与OS之间的关系,可以看成是这个样子的:
我们来解释一下:
1、从system call(系统调用结构)开始往下,都是属于OS和硬件的范畴,我们一般的程序员所进行的开发,通常都是在用户层。(这个我们后面还会具体说到)
2、其中,TCP/IP协议属于传输层,在该层的报文,是由操作系统来帮助我们收发。
3、而其他的MAC协议和以太网等协议,就是属于驱动、网卡等硬件设施层面的事情了。我们后面再来谈。他们属于数据链路层和物理层。
(注:这里只是初识,关于网络协议的分层我们后面会具体来说到)
二、网络发展史
略。
如果读者有兴趣,可以自己上网搜搜,大概就是说先是弄成个有线能够使得多台机器合作了,然后每个实验室每个实验室本质上都是属于一块局域网,最终经过发展,出现了一些协议,然后组织到了一起。
不过,
我们在这里可以扫个盲,说说运营商的概念。
在我国,有这么三大运营商,分别叫做:中国电信、中国联通和中国移动。
它们干了什么事情呢?
主要是:拉网线、建基站等。但是,正是由于这些工作,使得我们的网络可以通遍全国。这也是为啥非洲看不了抖音、快手等的原因。
所以,我们平时明明刷抖音、快手,玩游戏,明明用的是腾讯、字节等的产品,却到头来要给三大运营商交钱(流量费)。这实际上就是因为你用了人家的电信光缆等,是人家帮你搭建起的网线,给你建的基站,才有了如此迅猛发达的网络环境。
所以,我们可以得出一个结论,这其实也能够从一些政府文件当中看出:网络建设是一种基础设施建设。
三、协议
网络可以认为将我们所学的体系结构的线 变长了。
而计算机网络就是为了解决如何将数据从一个机器可靠地转移交付给另一个机器的过程。(就是解决线变长带来的问题)
关于协议,本质上就是一种约定。计算机协议就是计算机中的一种约定,是一种信息。
计算机之间的传输媒介是光信号和电信号. 通过 "频率" 和 "强弱" 来表示 0 和 1 这样的信息. 要想传递各种不同的信息, 就需要约定好双方的数据格式.
为啥呢?
计算机生产厂商有很多;
计算机操作系统, 也有很多;
计算机网络硬件设备, 还是有很多;
如何让这些不同厂商之间生产的计算机能够相互顺畅的通信? 就需要有人站出来, 约定一个共同的标准,大家都来遵守, 这就是 网络协议
3-1 协议主要是用来解决三个问题:
效率、可靠性、定位等(其他也有)
3-2 协议最终一定是通过结构体的形式描述组织起来;最终实际上发送、接收数据时一定比有效数据(有效载荷)多,因为其包含了协议。
故:协议+有效数据(载荷) = 数据报
网络协议在设计的时候,设计模式为层状结构,目的是为了实现“解耦”,使得可维护性更强。
层跟层之间用接口来实现关联。
四、OSI七层结构模型
物理层主要是解决有关网线、硬件设施等方面的问题。
数据链路层解决的是局域网之间的通信;
网络层解决的是跨网络层的。(路由器为代表)
传输层解决的是效率、可靠性等的问题。
上三层都是属于用户层,由于解决数据转换等问题。
五、TCP/IP四层(五层)协议结构模型
对于一台主机, 它的操作系统内核实现了从传输层到物理层(网络层和传输层)的内容;
对于一台路由器, 它实现了从网络层到物理层;
对于一台交换机, 它实现了从数据链路层到物理层;
对于集线器, 它只实现了物理层;
六、局域网中通信原理初识
1、每一层的都会将自己的报头添加到上一层传过来的数据中
2、传到链路层后,形成了一个完整的数据帧,
3、经过物理层,将该串数据解释成0/1,采用光电信号的形式,
4、扔到以太网中;然后经过某种方式,目标主机会接收到这份信号,然后重新解析成数据帧
封包、解包、分用、mac帧
1、解包的时候,需要解决报头和有效载荷 的分离问题
同时,
自底向上,需要确定自己的有效载荷需要交给上层的哪个协议。即分离之后,要将自己的有效载荷交付给上层的哪个协议——这种过程叫做分用
在局域网中,所有的主机都可以直接通讯。在数据链路层,有一个mac地址,即网卡硬件地址(全球唯一,用48个比特位来标识的)。
在发送数据的时候,每一个主机都可以接收到这份数据。然后对其进行解包,如果mac地址能够对的上,那么就留用,向上交付,否则就直接丢弃。
在局域网中互相通信的时候,就有可能发生数据碰撞,在整个局域网中,都是其碰撞域。一旦发生碰撞,这个时候传输的0/1序列就有可能发生变化。当然,接收方也是需要并且有方法来去进行检测的(这就和协议有关了)。所以,需要进行碰撞避免,可以采用暂缓或者是用交换机等方式 。
局域网中的广播,指所有的主机都可以收到该消息,类似于群发,所有的主机都进行接收、处理、交付。一般,广播地址设置为全F(或者理解为全1)
而交换机就是用来划分碰撞域的。
(想要黑掉局域网,就一直往局域网中塞垃圾数据:出现网络连接但不可用)
七、跨网络通信原理初始
稍稍总结一下:
不同的协议层对数据包有不同的称谓:
在传输层叫做段(segment),
在网络层叫做数据报 (datagram),
在链路层叫做帧(frame).
应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装
(Encapsulation).
首部信息中包含了一些类似于首部有多长, 载荷(payload)有多长, 上层协议是什么等信息.
数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部, 根据首部中的 "上层协议字段" 将数据交给对应的上层协议处理
下图为数据分用的过程
注意,在任何一层从来不关心上一层的报头是什么,只关心上层给来的东西,把上层给的统一当成数据(有效载荷)处理
八、认清分清相关概念
认识IP和MAC地址
IP有两个版本,IPV4和IPV6.我们下面默认的IP所指的都是IPV4
IP地址是在IP协议中, 用来标识网络中不同主机的地址;
对于IPv4来说, IP地址是一个4字节, 32位的整数;
我们通常也使用 "点分十进制" 的字符串表示IP地址, 例如 192.168.0.1 ;
用点分割的每一个数字表示一个字节, 范围是 0 - 255;(每一个分割的都是8个比特位)
MAC地址用来识别数据链路层中相连的节点;
长度为48位, 即6个字节. 一般用16进制数字加上冒号的形式来表示(例如: 08:00:27:03:fb:19)
在网卡出厂时就确定了, 不能修改. mac地址通常是唯一的(虚拟机中的mac地址不是真实的mac地址, 可能会冲突; 也有些网卡支持用户配置mac地址
源mac地址、目标mac地址:
在传到路由器之前:分别是发送主机和路由器;
到路由器之后:分别是路由器和目标主机。
源IP地址、目标IP地址(同上,但是目标IP地址是不发生变化的)
目标IP给我们提供的大的方向,最终要去的地方,而mac地址决定了我们下一跳往哪个地方走、进而如何一跳一跳地到达目标主机。
另外,站在TCP/UDP协议上,底层的协议都可以看成是IP协议,因为站在TCP/UDP协议的角度,底层是怎么实现的,其并不关心。
命令1:
在Linux当中如何查看本机的IP、mac地址以及网络信息:
①表示在局域网当中的ip地址;
②表示子网掩码;
③表示广播ip
④表示以太,这里可以表示mac帧地址,后面的表示接收队列能接收多少数据;
接下来的还有表示收到的数据包,收到的字节数是多少;包括错误的、丢包的等等
命令2:
lspci命令:
这里所表示的都是我们主机上挂着的硬件设备,就是说,都是些常见的外设。
注意:所有的网络数据转发,都必须经过硬件设备,在物理层上去跑。
不能有从用户层直接到用户层或者从传输层直接到传输层这样的过程。
九、套接字预备知识
我们所写的程序,都是在用户层进行的开发,或者说是在应用层进行协议的创作和规定。所以,我们所用的接口,是传输层的接口,也是系统调用接口 。也就是说,我们后面所学的套接字接口,所用到的都是传输层的接口。
我们两个主机之间的通信,是不是可以认为是两个主机硬件之间的通信呢?
答案是:既可以认为是,也可以认为不是。
实际上,硬件是承担两台主机进行通信的载体不错。
但实际上,进行通信的是两台主机里分别的进程。即为两个进程之间的通信,而这时软件层面上的。
【套接字、端口号】
我们今天,这里更加专业和准确的来说:套接字本质就是进程间通信(是指不同主机间的进程)。
而IP标识的主机的唯一性,但是在该主机上可能跑着许许多多的进程,那我怎么去确定我应该给哪一台主机呢?
所以,我们还要通过某种方式来去标识特定主机上的某种进程。而标识进程的方式我们就叫做端口号。
而用IP和端口号我们就能标识全网范围内的唯一的主机上面的唯一的进程
而 套接字 = IP地址+端口号(端口号是一个16位的比特位)
(如下图:)
【端口号和PID】
每个进程都会有PID,但是一个进程必须是要网络进程才会有端口号
【注意:一个端口号只能有一个进程占用,但是一个进程可以有多个端口号】
(10086就相当于IP地址,而某一个人工就相当于一个端口号,然后其身份证号就是相当于PID)
总结一下:
1、端口号是一个2字节16位的整数;
2、端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
3、IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
4、一个端口号只能被一个进程占用
十、TCP和UDP协议初识
TCP协议特点(初识):
传输层协议
有连接
可靠传输
面向字节流
UDP协议特点(初识):
传输层协议
无连接
不可靠传输
面向数据报
TCP协议需要解决数据传错了怎么办、丢包了怎么办、对方来不及接收怎么办、网络拥塞了怎么办,就是说,其会把大量的问题都要很好地解决,这样也就直接决定了TCP的特点(相对于UDP而言,其比较复杂),所以反过来,UDP就比较简单高效。
关于udp和tcp协议,我们后面会有详细专门的介绍。
网络字节序
我们的机器现在都是小端,但是有没有一种情况,我们这边是小端机,但是服务器是大端机,这样的话我们按照 小端字节序 发送出去的数据 到了对方的服务器上 以大端的方式接收 就有可能发生错乱。
那怎么样来解决这种问题呢?
我们网络规定:所有网络中所跑的数据,都是大端的数据。如果是小端机,需要先完成转化才能发送或者接收。然后先发送或者接收的数据是低地址的数据,再是高地址的数据。
总结来说,其:
1、发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
2、接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
3、因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
4、TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
5、不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据; 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为什么要规定为大端呢?
因为这样,我们先就可以接收到的就可以是低地址处的数据,即高位权值的数据。
这样,我们可以直接将高位放在高的位置上,可以实现边实现边计算。
同样,当我们接收字符串等消息时,接收的信息就可以按照我们视觉中的从左到右出现,而不是先接收到的最后一位,然后不断类似”头插“的操作。
即大端更符合我们的阅读习惯。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
十一、socket编程
socket编程接口
常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器) int socket(int domain, int type, int protocol);
参数解释:
第一个参数:domain表示一种域,具体来说即 在协议家族的某种协议
第二个参数:表明所创建套接字的类别。一般来说有三种:1、面向字节流(TCP);2、用户数据报(UDP);原始套接字(暂不说)
最后一个参数:代表一种协议,我们用默认的就可以,默认的为0。让OS使用默认的协议就可以。
其返回值:本质上为一个文件文件描述符(这和我们后面说高级IO类似)
注意:这里第二个参数:AF_INET这里的INET就是IPV4的意思。
而后面的16为端口号和32为IP地址就是我们的套接字
// 绑定端口号 (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结构
为什么会有这么一个结构?
原因很简单,其本质上,就是为了想让我们能用一种接口实现多种协议功能的创建。
左侧的即为通用的一个sockaddr的一个接口
第二个就是我们所要用的基于流式套接字或者是基于数据报套接字所要用到的接口
右边的是一个域间套接字,短期内我们基本不用。
注意,这里虽然有三种类型,但是它们的起始地址都是一样的(包括后面都是16为地址类型)。
所以,我们到时候想要用哪个,直接强转就可以了
我们通过上面的接口可以看到,实际上我们传递的时候,我们需要进行强制类型转化,因为我们用的不是sockaddr_in,而是sockaddr
还是那句话,其这么干的原因就是为了 在实现UDP/TCP等内容的时候,我们可以用同一组接口。类似于C++的函数重载、多态等概念。就是一套接口解决所有问题。
强转之后,我们可以直接用前面的16位地址类型就可以确认出是IPV4还是域间套接的方式。
我们再来说说socket套接字和bind绑定的关系:
1、socket为套接字信息,套接字和文件的关联信息也已经有了,就是说,我们这里的文件也就和网络关联起来了。这个网络文件所对应的IP地址和端口号也就已经有了。
2、socket本质上是向系统申请了一批我们需要在不同主机间通信的网络资源。
3、而这个网络资源还需要和我们具体的数据(即IP和端口号)结合起来,这样才能够发挥作用。
关于为什么用socket函数之后还需要bind绑定:
socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来。
只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。完成一些函数指针初始化等的一些操作。类似地,客户端也要用 connect() 函数建立连接。
客户端不需要绑定
并不是说客户端不需要IP和端口。而是客户端在发送套接字的时候,由OS自动完成
也就是说,我们不需要对其进行强绑定(注意:这里的绑定指的是绑定IP和端口号)
进一步解释也可以参照下图:
类比read函数 ,
第一个参数是一个所打开的套接字(文件描述符)(即读到一个套接字当中),buf是一个读数据的缓冲区,flag表示当没有数据时等待的方式(0表示阻塞式等待),后面两个参数是一对输出型参数,即包含的是发送过来的socket的信息。
解释一下上面的127.0.0.1:
查看网络状态命令:netstat
-nlup n表示以数字方式呈现;l可以理解为以列表方式呈现(后面可能会有不一样的理解方式——表示listen);u表示udp ;p表示process进程
我们优化一下,这里我们不需要具体的IP,直接用INADDR_ANY,表示与该主机相关的IP报文其都可以接受到。(这样就可能接收到的是公网IP,也有可能是局域网IP)
当我们的服务器接收到这样的信息之后,它便知道其发送者是谁。当与此同时
有多个客户端连接它时,它可以将这些客户端信息放到一个数组里,然后再遍历这个数组
将所有的消息遍历发出去。这样,就实现了群发的功能。
同样,我们可以分析命令,然后创建子进程,再程序替换; 同样,我们可以用dup2来去实现往socket里面重定向
inet_ntoa函数
这个函数做了两件事情:将四字节序的IP转换为点分十进制,同时,将网络序列转化为字节序列
转换方式:定义一个联合体,然后再定义一个位段,就可以直接实现分离。
(即先对ip赋值,然后后面的part1-part4就是点分十进制的四个不同的数,然后就直接将其转换为字符串再拼接在一起就可以了)
TCP服务器
(关于INADDR_ANY的定义:)
accept函数解释:
专注于把拿上来的链接进行通信
注意一下这里的sockfd和返回值的区别:
sockfd相当于是一个在门口招呼客人的引导员,至于具体是谁来招待你,那是另外你个里面的人了,这个里面的人就相当于返回值的这个文件描述符。
下面是参考的相关的文献,关于其解释的比较合理的地方,引用并标注了出来。
有一种比较老的连接服务器的方式 : 直接telnet 后面跟上端口号 /IP/网址
connect函数解释:
第二个和第三个参数表明你要连接谁。
在连接的时候,系统底层就自动为我们进行了绑定(实际上是进行三次握手,我们后面会说)
tcp服务器和udp服务器代码实现:
udp服务器:
udpServer.hpp
1 #pragma once 2 #include<stdlib.h> 3 #include<cstdio> 4 #include<unistd.h> 5 #include <iostream> 6 #include <string> 7 #include<arpa/inet.h> 8 #include<sys/types.h> 9 #include<sys/socket.h> 10 #include<netinet/in.h> 11 class udpServer 12 { 13 private: 14 // std::string ip; 15 int port; 16 int sock; 17 public: 18 udpServer(int _port = 8080) 19 ://ip(_ip) 20 port(_port) 21 {} 22 void InitServer() 23 { 24 sock = socket(AF_INET ,SOCK_DGRAM ,0);//系统信息我有,网络信息我有(都在这里创建的),但是二者之间没有关系,AF_INET 是一个宏的值,为2,代表其为ipv4的一个ip地址。 25 //SOCK_DGRAM表示其所采用的为udp协议 26 std::cout << "sock :" <<sock << std::endl; 27 struct sockaddr_in local;//到目前为止,其也只是保存在用户栈上的,我们要让操作系统拿到它,用到它,需要将其bind起来,弄到内核栈中去 28 local.sin_family = AF_INET; 29 local.sin_port = htons(port); 30 local.sin_addr.s_addr = INADDR_ANY; 31 //bind 将网络文件将文件和网络关联起来,即将系统信息和网络信息关联起来 32 if(bind(sock, (struct sockaddr*)&local, sizeof(local))<0) 33 //搞这么复杂,其本质上还是让我们能用一套接口完成所有的调用 34 { 35 std::cerr << "bind error\n"<<std::endl; 36 exit(1); 37 } 38 } 39 40 void start() 41 { 42 char msg[256]; 43 //std::string msg; 44 for(;;) 45 { 46 //我们来将客户端的ip地址和端口号拿出来看下: 47 msg[0] = 0; 48 struct sockaddr_in end_point; 49 socklen_t len = sizeof(end_point); 50 ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,\ 51 0,(struct sockaddr*)&end_point,&len); 52 if(s > 0) 53 { 54 char buf[16]; 55 sprintf(buf, "%d",ntohs(end_point.sin_port)); 56 std::string cli = inet_ntoa(end_point.sin_addr); 57 cli += ":"; 58 cli += buf; 59 msg[s] = '\0'; 60 std::cout <<cli<< "# "<<msg<<std::endl; 61 std::string echo_string = msg; 62 echo_string += "[server echo!]"; 63 sendto(sock,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&end_point,len); 64 } 65 } 66 } 67 68 ~udpServer() 69 { 70 close(sock); 71 } 72 };
udpServer.cc
1 #include "udpServer.hpp" 2 3 void Usage(std::string proc) 4 { 5 std::cout << "Usage :" <<proc << "loca_port "<<std::endl; 6 } 7 8 int main(int argc, char* argv[]) 9 { 10 if(argc != 2) 11 { 12 Usage(argv[0]); 13 exit(1); 14 } 15 udpServer* up = new udpServer(atoi(argv[1])); 16 up->InitServer(); 17 up->start(); 18 delete up; 19 return 0; 20 }
udpClient.hpp
1 #pragma once 2 #include<stdlib.h> 3 #include<unistd.h> 4 #include <iostream> 5 #include <string> 6 #include<arpa/inet.h> 7 #include<sys/types.h> 8 #include<sys/socket.h> 9 #include<netinet/in.h> 10 class udpClient 11 { 12 private: 13 std::string ip; 14 int port; 15 int sock; 16 public: 17 //ip,port ? 是server的 18 udpClient(std::string _ip = "127.0.0.1",int _port = 8080) 19 :ip(_ip) 20 ,port(_port) 21 {} 22 void InitClient() 23 { 24 sock = socket(AF_INET ,SOCK_DGRAM ,0);//系统信息我有,网络信息我有(都在这里创建的),但是二者之间没有关系,AF_INET 是一个宏的值,为2 25 std::cout << "sock :" <<sock << std::endl; 26 //客户端不需要绑定 27 //struct sockaddr_in local;//到目前为止,其也只是保存在用户栈上的,我们要让操作系统拿到它,用到它,需要将其bind起来,弄到内核栈中去 28 //local.sin_family = AF_INET; 29 //local.sin_port = htons(port); 30 //local.sin_addr.s_addr = inet_addr(ip.c_str()); 31 bind 将网络文件将文件和网络关联起来,即将系统信息和网络信息关联起来 32 //if(bind(sock, (struct sockaddr*)&local, sizeof(local))<0) 33 // //搞这么复杂,其本质上还是让我们能用一套接口完成所有的调用 34 //{ 35 // std::cerr << "bind error\n"<<std::endl; 36 // exit(1); 37 //} 38 } 39 40 void start() 41 { 42 //char msg[256]; 43 std::string msg; 44 struct sockaddr_in peer; 45 peer.sin_family = AF_INET; 46 peer.sin_port = htons(port); 47 peer.sin_addr.s_addr = inet_addr(ip.c_str()); 48 for(;;) 49 { 50 std::cout << "please Enter# "; 51 std::cin >>msg; 52 sendto(sock ,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer)); 53 char echo[128]; 54 ssize_t s = recvfrom(sock,echo,sizeof(echo)-1 ,0,nullptr,nullptr); 55 if(s > 0) 56 { 57 echo[s] = 0; 58 std::cout << "server# "<< echo <<std::endl; 59 } 60 61 // msg[0] = 0; 62 // struct sockaddr_in end_point; 63 // socklen_t len = sizeof(end_point); 64 // ssize_t s = recvfrom(sock,msg,sizeof(msg)-1,\ 65 // 0,(struct sockaddr*)&end_point,&len); 66 // if(s > 0) 67 // { 68 // msg[s] = '\0'; 69 // std::cout << "client# "<<msg<<std::endl; 70 // } 71 } 72 } 73 74 ~udpClient() 75 { 76 close(sock); 77 } 78 };
udpClient.cc
1 #include "udpClient.hpp" 2 3 void Usage(std::string proc) 4 { 5 std::cout << "Usage :" <<proc << "svr_ip svr_port "<<std::endl; 6 } 7 int main(int argc,char* argv[]) 8 { 9 if(argc != 3) 10 { 11 Usage(argv[0]); 12 exit(1); 13 } 14 udpClient uc(argv[1],atoi(argv[2])); 15 uc.InitClient(); 16 uc.start(); 17 return 0; 18 }
(由于每一行的代码前面有数字,所以建议各位自行实践一遍)
TCP服务器:
(这里所有的代码都是优化之后的最终版本)
tcpServer.hpp
1#pragma once 2 #include<iostream> 3 #include<pthread.h> 4 #include<unistd.h> 5 #include<string.h> 6 #include<string> 7 #include<stdlib.h> 8 #include<signal.h> 9 #include<sys/types.h> 10 #include<sys/socket.h> 11 #include<netinet/in.h> 12 #include<arpa/inet.h> 13 14 #define BACKLOG 5 15 16 17 class tcpServer{ 18 private: 19 int port; 20 int lsock; 21 public: 22 tcpServer(int _port):port(_port),lsock(-1) 23 {} 24 25 void InitServer() 26 { 27 signal(SIGCHLD,SIG_IGN); 28 lsock = socket(AF_INET,SOCK_STREAM,0); 29 if(lsock < 0) 30 { 31 std::cerr << "socket error" <<std::endl; 32 exit(2); 33 } 34 struct sockaddr_in local; 35 local.sin_family = AF_INET; 36 local.sin_port = htons(port); 37 local.sin_addr.s_addr = INADDR_ANY; 38 39 if(bind(lsock, (struct sockaddr*)&local, sizeof(local)) < 0) 40 { 41 std::cerr << "bind error" << std::endl; 42 exit(3); 43 } 44 45 if(listen(lsock,BACKLOG) < 0) 46 { 47 std::cerr << "bind error" << std::endl; 48 exit(4); 49 } 50 51 } 52 53 54 static void service(int sock) 55 { 56 57 char buffer[1024]; 58 while(true) 59 { 60 //可以用read和write,因为这个时候的链接已经在等待队列里等待获取调用了。 61 //send = write; recv = read; 62 size_t s = recv(sock,buffer,sizeof(buffer)-1,0); 63 if(s > 0) 64 { 65 buffer[s-1] = 0; 66 fflush(stdout); 67 std::cout << "client# " << buffer <<std::endl; 68 69 send(sock,buffer,strlen(buffer),0); 70 } 71 else if(s == 0) 72 { 73 std::cout << "client quit" <<std::endl; 74 close(sock); 75 break; 76 } 77 else 78 { 79 std::cout << "recv client data error..."<<std::endl; 80 break; 81 } 82 83 } 84 } 85 static void* serviceRoutine(void* arg) 86 { 87 pthread_detach(pthread_self()); 88 std::cout <<"create a new thread for IO"<<std::endl; 89 int* p = (int*)arg; 90 int sock = *p; 91 92 service(sock); 93 delete p; 94 } 95 96 void start() 97 { 98 99 sockaddr_in endpoint; 100 while(true) 101 { 102 std::cout <<"Please Enter Message"<<std::endl; 103 socklen_t len = sizeof(endpoint); 104 int sock = accept(lsock,(struct sockaddr*)&endpoint,&len); 105 if(sock < 0) 106 { 107 std::cerr<<"accept error"<<std::endl; 108 continue; 109 } 110 std::string cli_info = inet_ntoa(endpoint.sin_addr); 111 cli_info += ":"; 112 cli_info += std::to_string(ntohs(endpoint.sin_port)); 113 std::cout << "get a new link ..." << cli_info <<std::endl; 114 int* p = new int(sock); 115 pthread_t tid; 116 pthread_create(&tid,nullptr,serviceRoutine,(void*)p); 117 118 // pid_t id = fork(); 119 // if(id == 0) 120 // { 121 // close(lsock); 122 // service(sock); 123 // exit(0); 124 // } 125 // close(sock);//建议关闭:因为1、不要相信任何使用者;2、如果不关闭,会导致父进程可使用的文件描述符越来越少 126 } 127 } 128 129 130 131 132 133 ~tcpServer() 134 { 135 close(lsock); 136 } 137 };
tcpServer.cc
1 2 #include"tcpServer.hpp" 3 4 5 void Usage(std::string proc) 6 { 7 std::cout << "Usage :" <<std::endl; 8 std::cout << '\t' << proc << "port" <<std::endl; 9 10 } 11 12 13 int main(int argc, char* argv[]) 14 { 15 if(argc != 2) 16 { 17 Usage(argv[0]); 18 exit(1); 19 } 20 tcpServer * tp = new tcpServer(atoi(argv[1])); 21 tp->InitServer(); 22 tp->start(); 23 24 delete tp; 25 return 0; 26 } tcpClient.hpp 1 #pragma once 2 3 #include<iostream> 4 #include<stdlib.h> 5 #include<unistd.h> 6 #include<sys/types.h> 7 #include<sys/socket.h> 8 #include<netinet/in.h> 9 #include<arpa/inet.h> 10 #include<string> 11 #include<string.h> 12 class tcpClient 13 { 14 private: 15 int port;//服务器的端口号和ip 16 std::string ip; 17 int sock; 18 public: 19 tcpClient(std::string _ip = "127.0.0.1",int _port = 8080) 20 :port(_port),ip(_ip) 21 {} 22 23 void InitClient() 24 { 25 sock = socket(AF_INET,SOCK_STREAM,0); 26 if(sock < 0) 27 { 28 std::cerr << "socket error" << std::endl; 29 exit(2); 30 } 31 32 struct sockaddr_in svr; 33 svr.sin_family = AF_INET; 34 svr.sin_port = htons(port); 35 svr.sin_addr.s_addr = inet_addr(ip.c_str()); 36 if(connect(sock, (struct sockaddr*)&svr,sizeof(svr)) != 0) 37 { 38 std::cerr << "connect error" <<std::endl; 39 } 40 } 41 void start() 42 { 43 44 char msg[64]; 45 while(true) 46 { 47 size_t s = read(0,msg,sizeof(msg)-1); 48 if(s > 0) 49 { 50 msg[s] = 0; 51 send(sock,msg,strlen(msg),0); 52 size_t ss = recv(sock,msg,sizeof(msg)-1,0); 53 if(ss > 0) 54 { 55 msg[ss] = 0; 56 std::cout << "server echo #" << msg <<std::endl; 57 } 58 } 59 } 60 } 61 ~tcpClient() 62 { 63 close(sock); 64 } 65 };
tcpClient.cc
1 #include"tcpClient.hpp" 2 3 void Usage(std::string proc) 4 { 5 std::cout << "Usage:" << proc <<std::endl; 6 std::cout << '\t'<<"server_port server_ip"<<std::endl; 7 } 8 9 10 int main(int argc,char* argv[]) 11 { 12 if(argc != 3) 13 { 14 Usage(argv[0]); 15 exit(1); 16 } 17 18 19 tcpClient* tc = new tcpClient(); 20 tc->InitClient(); 21 tc->start(); 22 return 0; 23 }
我们最初的版本,当客户端退出的时候,服务器要么在recv阻塞,要么在send阻塞,反正是在某一个地方阻塞住了。
那么我们作为服务器,如何能够判断出外部的服务器链接退出了呢?
答案就在我们的recv上面。当recv的返回值为0的时候,就代表着客户端链接退出。
而当一个客户端链接退出的时候,我们应当让服务器从当循环中退出,然后关闭和客户端建立连接的套接字
而上面版本的又是单进程(线程)的。
不能支持同时连接多个客户端(想一下,为什么我们之前的udp可以?)
因为udp不管那么多事情,把报文扔出去的那一个瞬间,它的任务就完成了。也就是说,其是无连接的。
所以,我们进行了优化,将其改成了多进程的版本(我们在上面的代码略有提到)
代码运行实验与进程监视(ps aux | grep -E 'tcpClient'),如下:
(注:上述的ctrl + z表示将其放在后台运行,如上)
(退出方式) (fg + 序号)则可表示将其放在前台运行(注意:同一个时间前台运行的程序只能有一个,如上)
(也可以采用这样的监控脚本进行监控,如上)
(监视其网络状态,如下)
关于多进程的写法,我们也可以采用爷孙的方式(如下),但是这种方法不是很推荐,因为创建新的进程需要资源。
我们还可以考虑采用多线程的写法(即上述代码的最终版):
代码实验结果以及监视结果(对多线程进行监视 ps -aL)
也可以考虑采用这样的监控脚本:
总结一下,我们的服务器经历了:
(关于线程池的代码我们稍后再说)
短链接:
并且,向上面这种设计方法,实际上是基于短链接的:
每一次链接只完成一次任务,一次请求,一次回复,这样的 链接称为短链接。
我们将上述的接口现在对比着TCP通讯的流程,
来解释一下各个接口的作用:
首先,我们所用的模型通常被称之为CS模型:
即客户端主动连接服务端,而服务端不会主动连接客户端的这样一种连接方式
以TCP三次握手、四次挥手为例:
(先来说一下三次握手)
1、客户端向服务器发起SYN请求:客户端给服务器发了一个数据报,在数据报中有一个标志位SYN,如果经过了某种设置,那么其就是一个服务器连接请求
2、服务器进行相应,发出含有SYN+ACK的报文。这里的ACK即表明一种成功应答
3、客户端再次向服务器发出回应ACK,服务器接收后,三次握手完成。
(包括四次挥手后面具体再说)
我们这里要说的是其和系统调用接口的关系:
在服务器处于listen状态时,(即此时服务端已经建立了listen套接字)
1、connect本质上是触发客户端向服务器发送SYN的数据报,触发链接建立,即触发三次握手
2、三次握手之后,accept返回,即此时建立分配了新的文件描述符用于和客户端通信。
3、接下来,就是双方的数据接收和发送。Send = write; recv = read(注意,这里收发数据之后另一方需要ACK,即应答(为了保证可靠性,同时也是因为双方的通信地位时对等的))
4、紧接着,如果要断开连接,服务器和客户端都要调用close。客户端调用close是向服务器发送FIN,服务器自动ACK。而服务器调用close本质上时服务器给客户端调用FIN,然后客户端自动ACK.
TCP协议是全双工的。即接收端和发送端在地位上是平等的,即一端在发送的时候也可以进行发送数据。
与之对应的是半双工,即一端发送或者接收的时候,另一端只能够听着或者看着。
我们最后来理解一下文件描述符和套接字是怎么产生关联的(从内核代码的角度)
我们本质上调用了sys_socket等函数指针,然后创建了一个socket结构体, 而这个结构体里面又包含了一个文件指针和一个sock指针(文件指针指向描述符,sock指针则指向sock结构体)。
而sock结构体中有一个接收队列和写入队列的概念。
在tcp_sock和udp_sock中,第一个为inet_connection_sock,而在inet_connection_sock中,第一个又为sock指针。
所以说,这些关系只要强转一下,就可以直接转换了。
并且在file文件中,还有一个private_data,而它的定义就是sock。
即通过sock可以指向file,通过file也可以找到sock。