Go 用interface实现多态
Go 用interface实现多态
Go 语言并没有设计诸如虚函数、纯虚函数、继承、多重继承等概念,但它通过接口却非常优雅地支持了面向对象的特性。
多态是一种运行期的行为,它有以下几个特点:
- 一种类型具有多种类型的能力
- 允许不同的对象对同一消息做出灵活的反应
- 以一种通用的方式对待个使用的对象
- 非动态语言必须通过继承和接口的方式来实现
看一个实现了多态的代码例子:
1 | package main |
代码里先定义了 1 个 Person
接口,包含两个函数:
1 | job() |
然后,又定义了 2 个结构体,Student
和 Programmer
,同时,类型 *Student
、Programmer
实现了 Person
接口定义的两个函数。注意,*Student
类型实现了接口, Student
类型却没有。
之后,我又定义了函数参数是 Person
接口的两个函数:
1 | func whatJob(p Person) |
main
函数里先生成 Student
和 Programmer
的对象,再将它们分别传入到函数 whatJob
和 growUp
。函数中,直接调用接口函数,实际执行的时候是看最终传入的实体类型是什么,调用的是实体类型实现的函数。于是,不同对象针对同一消息就有多种表现,多态
就实现了。
更深入一点来说的话,在函数 whatJob()
或者 growUp()
内部,接口 person
绑定了实体类型 *Student
或者 Programmer
。根据前面分析的 iface
源码,这里会直接调用 fun
里保存的函数,类似于: s.tab->fun[0]
,而因为 fun
数组里保存的是实体类型实现的函数,所以当函数传入不同的实体类型时,调用的实际上是不同的函数实现,从而实现多态。
运行一下代码:
1 | I am a student. |
参考资料
【各种面向对象的名词】https://cyent.github.io/golang/other/oo/
linux网络编程-多路I/O转接服务器:epoll基础
epoll
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行。
epoll除了提供select/polld 的IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait
的调用,提高应用程序效率。
ulimit -a
:当前用户进程所能打开的最大文件描述符个数(缺省为1024)
可以使用cat命令查看一个进程可以打开的socket描述符上限cat /proc/fs/file-max
。
如有需要,可以通过修改配置文件的方式修改该上限值:
1 | sudo vi /etc/security/limits.conf |
基础API
epoll_create
会创建一个监听红黑树。
1 | #include <sys/epoll.h> |
epoll_ctl
操作监听红黑树:控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
1 | #include <sys/epoll.h> |
epoll_wait
等待所监控文件描述符上有事件的产生,类似于select()调用。
1 | #include <sys/epoll.h> |
epoll实现多路IO转接
1 | #include <stdio.h> |
linux网络编程-多路I/O转接服务器:poll
poll
为拓展监听的上限,可以使用poll
poll函数原型
1 | #include <poll.h> |
如果不再监控某个文件描述符时,可以把pollfd中,fd设置为-1,poll不再监控此pollfd,下次返回时,把revents设置为0。
poll实现服务器
1 | /* server.c */ |
poll总结
优点:
- 自带数组结构, 可以将监听事件集合和返回事件集合分开
- 可以拓展监听上限, 超出1024的限制
缺点:
- 不能跨平台, 只适合于Linux系统
- 无法直接定位到满足监听事件的文件描述符, 编码难度较大
linux网络编程-多路I/O转接服务器:select
设计思路
多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。实现方法主要有select, poll, epoll
。
这种思想类似CPU对IO的处理的发展历程, select的地位就像中断管理器, IO设备有中断请求时才通知CPU, 对应的, 只有当客户端有连接请求时才会通知server进行处理. 也就是说只要server收到通知, 就一定有数据待处理或连接待响应, 不会再被阻塞而浪费资源。
select函数
select能监听的文件描述符个数受限于
`FD_SETSIZE
,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力
函数原型分析:
1 | #include <sys/time.h> |
- 重点在于readfds:当客户端有数据发到服务器上时, 触发服务器的读事件. 后面两个一般传NULL
- 三个传入传出参数都是位图, 每个二进制位代表了一个文件描述符的状态
- 传入的是想监听的文件描述符集合(对应位置一), 传出来的是实际有事件发生的文件描述符集合(将没有事件发生的位置零)
- 返回值:
- 所有监听的文件描述符当中有事件发生的总个数(读写异常三个参数综合考虑)
- -1说明发生异常, 设置errno
操作文件描述符的函数:
1 | void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0 |
select实现多路IO转接设计思路
1 | listenFd=Socket(); //创建套接字 |
代码实现
1 | /* select.c */ |
客户端:
1 | /* client.c */ |
自定义数组提高效率
1 | #include <stdio.h> |
select优缺点
缺点:
监听上限受文件描述符显示, 最大1024个
要检测满足条件的fd, 要自己添加业务逻辑, 提高了编码难度
优点:
- 跨平台, 各种系统都能支持
linux网络编程-并发服务器
并发服务器
多进程并发服务器
使用多进程并发服务器时要考虑以下几点:
父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)
系统内创建进程个数(与内存大小相关)
进程创建过多是否降低整体服务性能(进程调度)
框架
1 | //框架 |
实现
1 | /* server.c */ |
1 | //client.c |
多线程并发服务器
在使用线程模型开发服务器时需考虑以下问题:
调整进程内最大文件描述符上限
线程如有共享数据,考虑线程同步
服务于客户端线程退出时,退出处理。(退出值,分离态)
系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
思路分析
1 | Socket(); //创建监听套接字lfd |
实现
1 | #include <stdio.h> |
linux-网络编程-使用TCP的C/S模型
基于TCP协议的客户端/服务器程序的一般流程
服务器
调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态。
客户端
调用socket()
初始化后,调用connect()
发出SYN
段并阻塞等待服务器应答,服务器应答一个SYN-ACK
段,客户端收到后从connect()
返回,同时应答一个ACK
段,服务器收到后从accept()
返回。
数据传输的过程
建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()
返回后立刻调用read()
,读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多的请求了,就调用close()
关闭连接,就像写端关闭的管道一样,服务器的read()
返回0,这样服务器就知道客户端关闭了连接,也调用close()
关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
实现
1 | //server 端,作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。 |
获取客户端的地址:
1 | printf("client IP:%s,client port:%d", |
client端实现:
1 | //client的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印 |
注意:套接字: 一个fd可以索引读写两个缓冲区
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
客户端和服务器启动后可以使用netstat命令查看链接情况:
netstat -apn | grep 9726
出错处理
为使错误处理的代码不影响主程序的可读性,把与socket相关的一些系统函数加上错误处理代码封装成新的函数,做成一个模块wrap.c。
封装的目的:在server.c编译过程中突出逻辑,将出错处理与逻辑分开。将原函数首字母大写进行错误处理,这样还可以跳转到原函数的manPage.
1 | //wrap.h |
1 | // wrap.c |
半关闭
由原来的双工通信变为了单工通信, 客户端只能接受数据(缓冲区中的数据)
实现原理:
关闭了客户端套接字的写缓冲区
之所以半关闭后Client仍能向Server发送ACK数据包, 是因为Client关闭的只是写缓冲, 连接还在
连接在内核层面, 写缓冲在用户层面
如果Server没有收到Client最后发来的ACK数据包, 它会一直发送FIN数据包, 直到Client回执为止
linux网络编程-socket模型创建
socket模型创建
socket()—创建一个套接字, 用fd或文件句柄索引
bind()—绑定IP和port
listen()—设置监听上限(同时与Server建立连接数)
accpet()—阻塞监听客户端连接(传入一个上面创建的套接字, 传出一个连接的套接字)
在客户端中的connect()中绑定IP和port, 并建立连接
socket函数
1 | #include <sys/types.h> /* See NOTES */ |
socket()
打开一个网络通讯端口,如果成功的话,就像open()
一样返回一个文件描述符,应用程序可以像读写文件一样用read/write
在网络上收发数据,如果socket()
调用出错则返回-1 对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol指定为0即可。
bind函数
1 | #include <sys/types.h> /* See NOTES */ |
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
bind()
的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。struct sockaddr *
是一个通用指针类型,addr
参数实际上可以接受多种协议的sockaddr
结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:
1 | struct sockaddr_in servaddr; |
首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。
listen函数
1 | #include <sys/types.h> /* See NOTES */ |
查看系统默认backlog:cat /proc/sys/net/ipv4/tcp_max_syn_backlog
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()
返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
connect函数
1 | #include <sys/types.h> |
客户端需要调用connect()
连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
accept函数
1 | #include <sys/types.h> |
三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。 addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。
服务器的结构一般如下:
1 | while (1) { |
整个是一个while
死循环,每次循环处理一个客户端连接。由于cliaddr_len
是传入传出参数,每次调用accept()之前应该重新赋初值。accept()
的参数listenfd
是先前的监听文件描述符,而accept()
的返回值是另外一个文件描述符connfd
,之后与客户端之间就通过这个connfd
通讯,最后关闭connfd
断开连接,而不关闭listenfd
,再次回到循环开头listenfd
仍然用作accept
的参数。accept()
成功返回一个文件描述符,出错返回-1。
linux网络编程-套接字
套接字
在Linux环境下,socket用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
既然是文件,那么理所当然的可以使用文件描述符引用套接字。与管道类似,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
在TCP/IP协议中,IP地址+TCP或UDP端口号
唯一标识网络通讯中的一个进程。IP地址+端口号
就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
套接字通信原理如下图所示:
在网络通信中,套接字一定是成对出现的。 一端的发送缓冲区对应对端的接收缓冲区。使用同一个文件描述符绑定发送缓冲区和接收缓冲区。
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)。
网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,定义网络数据流的地址过程:发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址规定为:先发出的数据是低地址,后发出的数据是高地址。
小端法: 高位存在高地址, 低位存在低地址(计算机本地采用)
大端法: 高位存在低地址, 低位存在高地址(网络通信采用)
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如,在UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
1 | #include<arpa/inet.h> |
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
IP地址转换函数
由于如192.168.45.2
的IP地址为点分十进制表示, 需要转化为uint32_t
型, 有现成的函数(IPv4和IPv6都可以转换,函数接口是void *addrptr):
1 | int inet_pton(int af,const char* src,void* dst);//p表示点分十进制的ip,n为网络上的二进制ip |
sockaddr地址结构
Pv4和IPv6的地址格式定义在netinet/in.h
中,IPv4地址用sockaddr_in
结构体表示,包括16位端口号和32位IP地址,IPv6地址用sockaddr_in6
结构体表示,包括16位端口号、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h
中,用sock-addr_un
结构体表示。
bind函数原型:
1 | #include<sys/types.h> |
sockaddr_in
相关定义:
1 | /*相关结构体定义,在man 7 ip*/ |
初始化方法:
1 | addr.sin_family=AF_INET/AF_INET6; |
网络基础-网络名词术语
网络名词术语解析
路由(route)
路由(名词)
- 数据包从源地址到目的地址所经过的路径,由一系列路由节点组成。
路由(动词)
- 某个路由节点为数据包选择投递方向的选路过程。
路由器工作原理
路由器(Router)是连接因特网中各局域网、广域网的设备,它会根据信道的情况自动选择和设定路由,以最佳路径,按前后顺序发送信号的设备。
传统地,路由器工作于OSI七层协议中的第三层,其主要任务是接收来自一个网络接口的数据包,根据其中所含的目的地址,决定转发到下一个目的地址。因此,路由器首先得在转发路由表中查找它的目的地址,若找到了目的地址,就在数据包的帧格前添加下一个MAC地址,同时IP数据包头的TTL(Time To Live)域也开始减数, 并重新计算校验和。当数据包被送到输出端口时,它需要按顺序等待,以便被传送到输出链路上。
路由器在工作时能够按照某种路由通信协议查找设备中的路由表。如果到某一特定节点有一条以上的路径,则基本预先确定的路由准则是选择最优(或最经济)的传输路径。由于各种网络段和其相互连接情况可能会因环境变化而变化,因此路由情况的信息一般也按所使用的路由信息协议的规定而定时更新。
网络中,每个路由器的基本功能都是按照一定的规则来动态地更新它所保持的路由表,以便保持路由信息的有效性。为了便于在网络间传送报文,路由器总是先按照预定的规则把较大的数据分解成适当大小的数据包,再将这些数据包分别通过相同或不同路径发送出去。当这些数据包按先后秩序到达目的地后,再把分解的数据包按照一定顺序包装成原有的报文形式。路由器的分层寻址功能是路由器的重要功能之一,该功能可以帮助具有很多节点站的网络来存储寻址信息,同时还能在网络间截获发送到远地网段的报文,起转发作用;选择最合理的路由,引导通信也是路由器基本功能;多协议路由器还可以连接使用不同通信协议的网络段,成为不同通信协议网络段之间的通信平台。
路由和交换之间的主要区别就是交换发生在OSI参考模型第二层(数据链路层),而路由发生在第三层,即网络层。这一区别决定了路由和交换在移动信息的过程 中需使用不同的控制信息,所以两者实现各自功能的方式是不同的。
路由表(Routing Table)
在计算机网络中,路由表或称路由择域信息库(RIB)是一个存储在路由器或者联网计算机中的电子表格(文件)或类数据库。路由表存储着指向特定网络地址的路径。
路由条目
路由表中的一行,每个条目主要由目的网络地址、子网掩码、下一跳地址、发送接口四部分组成,如果要发送的数据包的目的网络地址匹配路由表中的某一行,就按规定的接口发送到下一跳地址。
缺省路由条目
路由表中的最后一行,主要由下一跳地址和发送接口两部分组成,当目的地址与路由表中其它行都不匹配时,就按缺省路由条目规定的接口发送到下一跳地址。
路由节点
一个具有路由能力的主机或路由器,它维护一张路由表,通过查询路由表来决定向哪个接口发送数据包。
以太网交换机工作原理
以太网交换机是基于以太网传输数据的交换机,以太网采用共享总线型传输媒体方式的局域网。以太网交换机的结构是每个端口都直接与主机相连,并且一般都工作在全双工方式。交换机能同时连通许多对端口,使每一对相互通信的主机都能像独占通信媒体那样,进行无冲突地传输数据。
以太网交换机工作于OSI网络参考模型的第二层(即数据链路层),是一种基于MAC(Media Access Control,介质访问控制)地址识别、完成以太网数据帧转发的网络设备。
hub工作原理
集线器实际上就是中继器的一种,其区别仅在于集线器能够提供更多的端口服务,所以集线器又叫多口中继器。
集线器功能是随机选出某一端口的设备,并让它独占全部带宽,与集线器的上联设备(交换机、路由器或服务器等)进行通信。从Hub的工作方式可以看出,它在网络中只起到信号放大和重发作用,其目的是扩大网络的传输范围,而不具备信号的定向传送能力,是—个标准的共享式设备。其次是Hub只与它的上联设备(如上层Hub、交换机或服务器)进行通信,同层的各端口之间不会直接进行通信,而是通过上联设备再将信息广播到所有端口上。 由此可见,即使是在同一Hub的不同两个端口之间进行通信,都必须要经过两步操作:
第一步是将信息上传到上联设备;
第二步是上联设备再将该信息广播到所有端口上。
半双工/全双工
Full-duplex(全双工)全双工是在通道中同时双向数据传输的能力。
Half-duplex(半双工)在通道中同时只能沿着一个方向传输数据。
DNS服务器
DNS 是域名系统 (Domain Name System) 的缩写,是因特网的一项核心服务,它作为可以将域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP地址串。
它是由解析器以及域名服务器组成的。域名服务器是指保存有该网络中所有主机的域名和对应IP地址,并具有将域名转换为IP地址功能的服务器。
局域网(LAN)
local area network,一种覆盖一座或几座大楼、一个校园或者一个厂区等地理区域的小范围的计算机网。
覆盖的地理范围较小,只在一个相对独立的局部范围内联,如一座或集中的建筑群内。
使用专门铺设的传输介质进行联网,数据传输速率高(10Mb/s~10Gb/s)
通信延迟时间短,可靠性较高
局域网可以支持多种传输介质
广域网(WAN)
wide area network,一种用来实现不同地区的局域网或城域网的互连,可提供不同地区、城市和国家之间的计算机通信的远程计算机网。
覆盖的范围比局域网(LAN)和城域网(MAN)都广。广域网的通信子网主要使用分组交换技术。
广域网的通信子网可以利用公用分组交换网、卫星通信网和无线分组交换网,它将分布在不同地区的局域网或计算机系统互连起来,达到资源共享的目的。如互联网是世界范围内最大的广域网。
适应大容量与突发性通信的要求;
适应综合业务服务的要求;
开放的设备接口与规范化的协议;
完善的通信服务与网络管理。
端口
逻辑意义上的端口,一般是指TCP/IP协议中的端口,端口号的范围从0到65535,比如用于浏览网页服务的80端口,用于FTP服务的21端口等等。
端口号小于256的定义为常用端口,服务器一般都是通过常用端口号来识别的。
客户端只需保证该端口号在本机上是惟一的就可以了。客户端口号因存在时间很短暂又称临时端口号;
大多数TCP/IP实现给临时端口号分配1024—5000之间的端口号。大于5000的端口号是为其他服务器预留的。
在自定义端口时,避免使用well-known的端口。如:80、21等等。
MTU
MTU:通信术语 最大传输单元(Maximum Transmission Unit,MTU)
是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡、串口等)。
eg: 以太网(Ethernet)协议的MTU为1500字节