Go 用interface实现多态

Go 语言并没有设计诸如虚函数、纯虚函数、继承、多重继承等概念,但它通过接口却非常优雅地支持了面向对象的特性。

多态是一种运行期的行为,它有以下几个特点:

  1. 一种类型具有多种类型的能力
  2. 允许不同的对象对同一消息做出灵活的反应
  3. 以一种通用的方式对待个使用的对象
  4. 非动态语言必须通过继承和接口的方式来实现

看一个实现了多态的代码例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import "fmt"

func main() {
qcrao := Student{age: 18}
whatJob(&qcrao)

growUp(&qcrao)
fmt.Println(qcrao)

stefno := Programmer{age: 100}
whatJob(stefno)

growUp(stefno)
fmt.Println(stefno)
}

func whatJob(p Person) {
p.job()
}

func growUp(p Person) {
p.growUp()
}

type Person interface {
job()
growUp()
}

type Student struct {
age int
}

func (p Student) job() {
fmt.Println("I am a student.")
return
}

func (p *Student) growUp() {
p.age += 1
return
}

type Programmer struct {
age int
}

func (p Programmer) job() {
fmt.Println("I am a programmer.")
return
}

func (p Programmer) growUp() {
// 程序员老得太快 ^_^
p.age += 10
return
}

代码里先定义了 1 个 Person 接口,包含两个函数:

1
2
job()
growUp()

然后,又定义了 2 个结构体,StudentProgrammer,同时,类型 *StudentProgrammer 实现了 Person 接口定义的两个函数。注意,*Student 类型实现了接口, Student 类型却没有。

之后,我又定义了函数参数是 Person 接口的两个函数:

1
2
func whatJob(p Person)
func growUp(p Person)

main 函数里先生成 StudentProgrammer 的对象,再将它们分别传入到函数 whatJobgrowUp。函数中,直接调用接口函数,实际执行的时候是看最终传入的实体类型是什么,调用的是实体类型实现的函数。于是,不同对象针对同一消息就有多种表现,多态就实现了。

更深入一点来说的话,在函数 whatJob() 或者 growUp() 内部,接口 person 绑定了实体类型 *Student 或者 Programmer。根据前面分析的 iface 源码,这里会直接调用 fun 里保存的函数,类似于: s.tab->fun[0],而因为 fun 数组里保存的是实体类型实现的函数,所以当函数传入不同的实体类型时,调用的实际上是不同的函数实现,从而实现多态。

运行一下代码:

1
2
3
4
I am a student.
{19}
I am a programmer.
{100}

参考资料

【各种面向对象的名词】https://cyent.github.io/golang/other/oo/

【多态与鸭子类型】https://www.jb51.net/article/116025.htm

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
2
3
4
5
sudo vi /etc/security/limits.conf

在文件尾部写入以下配置,soft软限制,hard硬限制:
* soft nofile 65536
* hard nofile 100000

基础API

epoll_create

会创建一个监听红黑树。

1
2
3
#include <sys/epoll.h>
int epoll_create(int size) size:监听数目
//返回值: 成功返回指向新创建的红黑树的根节点的fd, 失败返回-1并设置errno;

epoll_ctl

操作监听红黑树:控制某个epoll监控的文件描述符上的事件:注册、修改、删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
/*
epfd:为epoll_creat的句柄
op: 表示动作,用3个宏来表示:
EPOLL_CTL_ADD (注册新的fd到epfd),
EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
EPOLL_CTL_DEL (从epfd删除一个fd,取消监听);
fd:待监听的fd
event: 告诉内核需要监听的事件
*/
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR: 表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

epoll_wait

等待所监控文件描述符上有事件的产生,类似于select()调用。

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
/*
epfd: epoll_create的返回值
events: 传出参数, 是一个数组, 满足监听条件的那些fd结构体,用来存内核得到事件的集合。
maxevents: 数组中元素的总个数,告知内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
timeout: 超时时间
-1: 阻塞
0: 立即返回,非阻塞
>0: 指定毫秒
返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
*/

epoll实现多路IO转接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 9527
#define OPEN_MAX 1024

int main(int argc, char *argv[])
{
int i, j, maxi, listenfd, connfd, sockfd;
int nready, efd, res;
ssize_t n;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
int client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
/*创建一个临时节点temp和一个数组ep*/
struct epoll_event tep, ep[OPEN_MAX];

/*创建监听套接字*/
listenfd = Socket(AF_INET, SOCK_STREAM, 0);

/*设置地址可复用*/
int opt=1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt));

/*初始化服务器地址结构*/
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

/*绑定服务器地址结构*/
Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
/*设置监听上限*/
Listen(listenfd, 20);

for (i = 0; i < OPEN_MAX; i++)
client[i] = -1;
maxi = -1;

/*创建监听红黑树*/
efd = epoll_create(OPEN_MAX);
if (efd == -1)
perr_exit("epoll_create");

/*将listenFd加入监听红黑树中*/
tep.events = EPOLLIN; tep.data.fd = listenfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);
if (res == -1)
perr_exit("epoll_ctl");

while (1) {
/*阻塞监听写事件*/
nready = epoll_wait(efd, ep, OPEN_MAX, -1); /* 阻塞监听 */
if (nready == -1)
perr_exit("epoll_wait");

/*轮询整个数组(红黑树)*/
for (i = 0; i < nready; i++) {
if (!(ep[i].events & EPOLLIN))
continue;
/*如果是建立连接请求*/
if (ep[i].data.fd == listenfd) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (j = 0; j < OPEN_MAX; j++) {
if (client[j] < 0) {
client[j] = connfd; /* save descriptor */
break;
}
}

if (j == OPEN_MAX)
perr_exit("too many clients");
if (j > maxi)
maxi = j; /* max index in client[] array */

/*将新创建的连接套接字加入红黑树*/
tep.events = EPOLLIN;
tep.data.fd = connfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
if (res == -1)
perr_exit("epoll_ctl");
} else {
/*不是建立连接请求,是数据处理请求*/
sockfd = ep[i].data.fd;
/*读到0说明客户端关闭*/
n = Read(sockfd, buf, MAXLINE);
if (n == 0) {
for (j = 0; j <= maxi; j++) {
if (client[j] == sockfd) {
client[j] = -1;
break;
}
}
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
if (res == -1)
perr_exit("epoll_ctl");

Close(sockfd);
printf("client[%d] closed connection\n", j);
} else {
// 数据处理
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Writen(sockfd, buf, n);
}
}
}
}
close(listenfd);
close(efd);
return 0;
}

poll

为拓展监听的上限,可以使用poll

poll函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

/*
fds 监听的文件描述符数组
nfds 监控数组中有多少文件描述符需要被监控
timeout 毫秒级等待
-1:阻塞等,#define INFTIM -1 Linux中没有定义此宏
0:立即返回,不阻塞进程
>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值
返回值:返回满足对应监听事件的文件描述符总个数;
*/

struct pollfd {
int fd; /* 待监听的文件描述符 */
short events; /*待监听的文件描述符对应的监听事件->POLLIN,POLLOUT,POLLERR*/
short revents; /*returned events:传入时给0,如果满足对应事件的话被置为非零->POLLIN,POLLOUT,POLLERR*/
};

POLLIN 普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND
POLLRDNORM 数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级可读数据
POLLOUT 普通或带外数据可写
POLLWRNORM 数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件

如果不再监控某个文件描述符时,可以把pollfd中,fd设置为-1,poll不再监控此pollfd,下次返回时,把revents设置为0。

poll实现服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 9527
//可以突破select监听的1024限制
#define OPEN_MAX 1024

int main(int argc, char *argv[])
{
int i, j, maxi, listenfd, connfd, sockfd;
/*poll函数返回值*/
int nready;
ssize_t n;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
/*创建结构体数组*/
struct pollfd client[OPEN_MAX];
/*创建客户端地址结构和服务器地址结构*/
struct sockaddr_in cliaddr, servaddr;

/*得到监听套接字listenFd*/
listenfd = Socket(AF_INET, SOCK_STREAM, 0);

/*向服务器地址结构填入内容*/
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

/*绑定服务器地址结构到监听套接字,并设置监听上限*/
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20);

/*初始化第一个pollfd为监听套接字*/
client[0].fd = listenfd;
client[0].events = POLLRDNORM; /* listenfd监听普通读事件 */

for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* 用-1初始化client[]里剩下元素 */
maxi = 0; /* client[]数组有效元素中最大元素下标 */

for ( ; ; ) {
/*nready是有多少套接字有POLLIN请求*/
nready = poll(client, maxi+1, -1); /* 阻塞 */
/*如果listenFd的revents有POLLIN请求,则调用Accept函数得到connectFd*/
if (client[0].revents & POLLRDNORM) { /* 有客户端连接请求 */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 1; i < OPEN_MAX; i++) {
if (client[i].fd < 0) {
client[i].fd = connfd; /* 找到client[]中空闲的位置,存放accept返回的connfd */
break;
}
}

if (i == OPEN_MAX)
perr_exit("too many clients");

client[i].events = POLLRDNORM; /* 设置刚刚返回的connfd,监控读事件 */
if (i > maxi)
maxi = i; /* 更新client[]中最大元素下标 */
if (--nready <= 0)
continue; /* 没有更多就绪事件时,继续回到poll阻塞 */
}
/*开始从1遍历pollfd数组*/
for (i = 1; i <= maxi; i++) { /* 检测client[] */
/*到结尾了或者有异常*/
if ((sockfd = client[i].fd) < 0)
continue;
/*第i个客户端有连接请求,进行处理*/
if (client[i].revents & (POLLRDNORM | POLLERR)) {
if ((n = Read(sockfd, buf, MAXLINE)) < 0) {
/*出错时进一步判断errno*/
if (errno == ECONNRESET) { /* 当收到 RST标志时 */
/* connection reset by client */
printf("client[%d] aborted connection\n", i);
Close(sockfd);
client[i].fd = -1;
} else {
perr_exit("read error");
}
} else if (n == 0) {
/*read返回0,说明读到了结尾,关闭连接*/
/* connection closed by client */
printf("client[%d] closed connection\n", i);
Close(sockfd);
client[i].fd = -1;
} else {
/*数据处理*/
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Writen(sockfd, buf, n);
}
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
return 0;
}

poll总结

优点:

  • 自带数组结构, 可以将监听事件集合和返回事件集合分开
  • 可以拓展监听上限, 超出1024的限制

缺点:

  • 不能跨平台, 只适合于Linux系统
  • 无法直接定位到满足监听事件的文件描述符, 编码难度较大

设计思路

多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。实现方法主要有select, poll, epoll

这种思想类似CPU对IO的处理的发展历程, select的地位就像中断管理器, IO设备有中断请求时才通知CPU, 对应的, 只有当客户端有连接请求时才会通知server进行处理. 也就是说只要server收到通知, 就一定有数据待处理或连接待响应, 不会再被阻塞而浪费资源

select函数

  • select能监听的文件描述符个数受限于`FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数

  • 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力

函数原型分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set* readfds, fd_set* writefds,fd_set* exceptfds, struct timeval* timeout);

/*
nfds: 监控的文件描述符集里最大文件描述符加1,此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
*/
  • 重点在于readfds:当客户端有数据发到服务器上时, 触发服务器的读事件. 后面两个一般传NULL
  • 三个传入传出参数都是位图, 每个二进制位代表了一个文件描述符的状态
  • 传入的是想监听的文件描述符集合(对应位置一), 传出来的是实际有事件发生的文件描述符集合(将没有事件发生的位置零)
  • 返回值:
    • 所有监听的文件描述符当中有事件发生的总个数(读写异常三个参数综合考虑)
    • -1说明发生异常, 设置errno

操作文件描述符的函数:

1
2
3
4
void FD_CLR(int fd, fd_set *set); 	//把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0

select实现多路IO转接设计思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
listenFd=Socket();								//创建套接字
Bind(); //绑定地址结构
Listen(); //设置监听上限
fd_set rset; //创建读监听集合
fd_set allset;
FD_ZERO(&allset); //将读监听集合清空
FD_SET(listenFd,&allset); //将listenFd添加到所有读集合当中
while(1){
rset=allset; //保存监听集合
ret=select(listenFd,&rset,NULL,NULL,NULL); //监听文件描述符集合对应事件
if(ret>0){
if(FD_ISSET(listenFd,&rset)){
cfd=accept();
FD_SET(cfd,&allset); //添加到监听通信描述符集合中
}
for(i=listenFd+1;i<=cfd;++i){
FD_ISSET(i,&rset); //有read,write事件
read();
toupper();
write();
}
}
}

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/* select.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "wrap.h"
#define SERVER_PORT 9527

int main(int argc,char* argv[]){
int listenFd,connectFd;
char buf[BUFSIZ];

struct sockaddr_in serverAddr,clientAddr;
socklen_t clientAddrLen;
/*创建一个监听套接字*/
listenFd=Socket(AF_INET,SOCK_STREAM,0);
/*设置端口复用*/
int opt=1;
setsockopt(listenFd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt));
/*设置地址结构*/
bzero(&serverAddr,sizeof(serverAddr));
serverAddr.sin_family=AF_INET;
serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
serverAddr.sin_port=htons(SERVER_PORT);
/*绑定地址结构*/
Bind(listenFd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
Listen(listenFd,128);
/*定义读集合和备份集合并清空*/
fd_set rset,allset;
FD_ZERO(&allset);
FD_SET(listenFd,&allset);
int ret=0;
int maxfd=listenFd;
int i=0;
int j=0;
int n=0;

while(1){
/*备份*/
rset=allset;
/*使用select监听*/
ret=select(maxfd+1,&rset,NULL,NULL,NULL);
/*出错返回*/
if(ret==-1)
perr_exit("select error");
/*listen满足监听的事件*/
if(FD_ISSET(listenFd,&rset)){
clientAddrLen=sizeof(clientAddr);
/*建立链接,不会阻塞*/
connectFd=Accept(listenFd,(struct sockaddr*)&clientAddr,&clientAddrLen);
/*将connectFd加入集合*/
FD_SET(connectFd,&allset);
/*更新最大值*/
if(maxfd<connectFd)
maxfd=connectFd;
/*如果只有listen事件,只需建立连接即可,无需数据传输,跳出循环剩余部分*/
if(ret==1)
continue;
}

/*否则,说明有数据传输需求*/
for(i=listenFd+1;i<=maxfd;++i){
if(FD_ISSET(i,&rset)){
n=read(i,buf,sizeof(buf));
if(n==-1)
perr_exit("read error");
else if(n==0){
close(i);
FD_CLR(i,&allset);
}
for(j=0;j<n;++j)
buf[j]=toupper(buf[j]);
write(i,buf,n);
write(STDOUT_FILENO,buf,n);
}
}
}
close(listenFd);
return 0;
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 9527

int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;

sockfd = Socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);

Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}

自定义数组提高效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>

#include "wrap.h"

#define SERVER_PORT 9527
int main(int argc, char *argv[]){
int i, j, n, maxi;
/*将需要轮询的客户端套接字放入数组client[FD_SETSIZE]*/
int nready, client[FD_SETSIZE];
int listenFd, connectFd, maxFd, socketFd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN];

struct sockaddr_in serverAddr, clientAddr;
socklen_t clientAddrLen;
/*得到监听套接字*/
listenFd = Socket(AF_INET, SOCK_STREAM, 0);
/*定义两个集合,将listenFd放入allset集合当中*/
fd_set rset, allset;
FD_ZERO(&allset);
FD_SET(listenFd, &allset);
/*设置地址复用*/
int opt = 1;
setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
/*填写服务器地址结构*/
bzero(&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(SERVER_PORT);
/*绑定服务器地址结构*/
Bind(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
Listen(listenFd, 128);
/*将listenFd设置为数组中最大的Fd*/
maxFd = listenFd;
maxi = -1;
/*初始化自己的数组为-1*/
for (i = 0; i < FD_SETSIZE; ++i)
client[i] = -1;

while (1){
/*把allset给rest*/
rset = allset;
nready = select(maxFd + 1, &rset, NULL, NULL, NULL);

if (nready == -1)
perr_exit("select error");

/*如果有了新的连接请求,得到connectFd,并将其放入自定义数组中*/
if (FD_ISSET(listenFd, &rset)){
clientAddrLen = sizeof(clientAddr);
connectFd = Accept(listenFd, (struct sockaddr *)&clientAddr, &clientAddrLen);
printf("Recived from %s at PORT %d\n", inet_ntop(AF_INET, &(clientAddr.sin_addr.s_addr), str, sizeof(str)), ntohs(clientAddr.sin_port));

for (i = 0; i < FD_SETSIZE; ++i)
if (client[i] < 0){
client[i] = connectFd;
break;
}
/*自定义数组满了*/
if(i==FD_SETSIZE){
fputs("Too many clients\n",stderr);
exit(1);
}
/*connectFd加入监听集合*/
FD_SET(connectFd, &allset);

/*更新最大的Fd*/
if (maxFd < connectFd)
maxFd = connectFd;
/*更新循环上限*/
if(i>maxi)
maxi=i;
/*select返回1,说明只有建立连接请求,没有数据传送请求,跳出while循环剩余部分(下面的for循环轮询过程)*/
if (--nready == 0)
continue;
}
/*select返回不是1,说明有connectFd有数据传输请求,遍历自定义数组*/
for (i = 0; i <= maxi; ++i){
if((socketFd=client[i])<0)
continue;
/*遍历检查*/
if (FD_ISSET(socketFd, &rset)){
/*read返回0说明传输结束,关闭连接*/
if ((n=read(socketFd,buf,sizeof(buf)))==0){
close(socketFd);
FD_CLR(socketFd, &allset);
client[i]=-1;
}else if(n>0){
for (j = 0; j < n; ++j)
buf[j] = toupper(buf[j]);
write(socketFd, buf, n);
write(STDOUT_FILENO, buf, n);
}
/*不懂:需要处理的个数减1?*/
if(--nready==0)
break;
}
}
}
close(listenFd);
return 0;
}

select优缺点

缺点:

  • 监听上限受文件描述符显示, 最大1024个

  • 要检测满足条件的fd, 要自己添加业务逻辑, 提高了编码难度

优点:

  • 跨平台, 各种系统都能支持

并发服务器

并发服务器

多进程并发服务器

使用多进程并发服务器时要考虑以下几点:

  • 父进程最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符)

  • 系统内创建进程个数(与内存大小相关)

  • 进程创建过多是否降低整体服务性能(进程调度)

框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//框架
Socket(); //创建监听套接字lfd
Bind(); //绑定服务器地址结构
Listen(); //设置监听上限
while(1){
cfd=Accept();
pid=fork();
if(pid==0){
close(lfd); //子进程用不到lfd
read(cfd);
数据处理;
write(cfd);
}else if(pid>0){
close(cfd); //父进程用不到cfd
}
}
/*
子进程
close(lfd)
read()
数据处理
wirte()

父进程
注册信号捕捉函数:SIGNAL
在回调函数中完成子进程回收:while(waitpid())
*/

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/* server.c */
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 800

/*信号捕捉函数:回收子进程*/
void do_sigchild(int num)
{
while (waitpid(0, NULL, WNOHANG) > 0)
;
}

int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
pid_t pid;

struct sigaction newact;
newact.sa_handler = do_sigchild;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGCHLD, &newact, NULL);

listenfd = Socket(AF_INET, SOCK_STREAM, 0);
/*服务器地址结构*/
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
/*绑定服务器地址结构*/
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
/*设置监听上限*/
Listen(listenfd, 20);

printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);

pid = fork();
if (pid == 0) {
Close(listenfd);
while (1) {
n = Read(connfd, buf, MAXLINE);
if (n == 0) {
printf("the other side has been closed.\n");
break;
}
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
Write(connfd, buf, n);
}
Close(connfd);
return 0;
} else if (pid > 0) {
Close(connfd);
} else
perr_exit("fork");
}
Close(listenfd);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 6666

int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;

sockfd = Socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);

Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0) {
printf("the other side has been closed.\n");
break;
} else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}

多线程并发服务器

在使用线程模型开发服务器时需考虑以下问题:

  • 调整进程内最大文件描述符上限

  • 线程如有共享数据,考虑线程同步

  • 服务于客户端线程退出时,退出处理。(退出值,分离态)

  • 系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU

思路分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Socket();		//创建监听套接字lfd
Bind(); //绑定服务器地址结构
Listen(); //设置监听上限
while(1){
cfd=Accept(lfd,);
pthread_create(&tid,NULL,&tfn,NULL);
/*
*detach设置线程分离,但是这样不能获取线程退出状态
*如果想获取子线程退出状态,用pthread_join()函数,但是这样会造成主线程阻塞
*解决方案:create出一个新的子线程调用pthread_join()专门用于回收
*/
pthread_detach(tid);
}

//子线程:
void* tfn(void* arg){
close(lfd);
read(cfd,);
数据处理;
write(cfd,);
pthread_exit((void*)out); //线程退出状态
}

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666

/*将客户端的地址结构和对应的套接字封装到一个结构体中用于向子线程传参*/
struct s_info {
struct sockaddr_in cliaddr;
int connfd;
};

/*子线程的回调函数,注意参数类型*/
void *do_work(void *arg)
{
int n,i;
/*将参数接收下来*/
struct s_info *ts = (struct s_info*)arg;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
/* 可以在创建线程前设置线程创建属性,设为分离态,*/
pthread_detach(pthread_self());
while (1) {
n = Read(ts->connfd, buf, MAXLINE);
/*读到0,说明客户端已经断开连接*/
if (n == 0) {
printf("the other side has been closed.\n");
break;
}
/*打印客户端的信息*/
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
ntohs((*ts).cliaddr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
Write(ts->connfd, buf, n);
}
/*从循环跳出时,关闭套接字,退出线程*/
Close(ts->connfd);
}

int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
int i = 0;
pthread_t tid;
struct s_info ts[256];
/*创建监听套接字*/
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
/*初始化服务器地址结构*/
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

/*绑定服务器地址结构并设置监听上限*/
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20);

printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
/*拿到客户端信息后,填写到结构体中*/
ts[i].cliaddr = cliaddr;
ts[i].connfd = connfd;
/* 达到线程最大数时,pthread_create出错处理, 增加服务器稳定性 */
pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
i++;
}
return 0;
}

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

TCP协议下的C/S流程

服务器

调用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//server 端,作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERV_PORT 9726

/*错误处理函数*/
void sys_err(const char* str){
perror(str);
exit(1);
}

int main(){
int link_fd=0; //建立连接的socket文件描述符
int connect_fd=0 //用于通信的文件描述符
int ret=0; //用于检查是否出错
char buf[BUFSIZ]; //缓冲区
char client_IP[1024] //存入客户端IP字符串
int num=0; //读出的字节数
/*服务器端地址结构*/
struct sockaddr_in serv_addr;
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERV_PORT);
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);

/*成功与服务器建立连接的客户端地址结构*/
struct sockaddr_in clint_addr;
socklen_t clint_addr_len=sizeof(clint_addr);

/*socket函数:创建用于建立连接的socket,返回的文件描述符存入link_fd*/
link_fd=socket(AF_INET,SOCK_STREAM,0);
if(link_fd==-1)
sys_err("socket error");

/*bind函数:绑定服务器端的地址结构*/
ret=bind(link_fd,(const struct sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret==-1)
sys_err("bind error");

/*listen函数:设定监听(连接)上线*/
ret=listen(link_fd,128);
if(ret==-1)
sys_err("listen error");

/*accept函数:阻塞等待客户端建立连接*/
connect_fd=accept(link_fd,(struct sockaddr*)&clint_addr,&clint_addr_len);
if(connect_fd==-1)
sys_err("accept error");
/*建立连接后打印客户端的IP和端口号*/
printf("client IP:%s,client port:%d",
inet_ntop(AF_INET,&clint_addr.sin_addr.s_addr,client_IP,sizeof(client_IP)),
ntohs(clint_addr.sin_port));

/*业务逻辑*/
while(1){
num=read(connect_fd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,num);
for(i=0;i<num;i++)
buf[i]=toupper(buf[i]);
write(connect_fd,buf,num);
sleep(1);
}
close(connect_fd);
close(link_fd);
return 0;
}

获取客户端的地址:

1
2
3
printf("client IP:%s,client port:%d",
inet_ntop(AF_INET,&clint_addr.sin_addr.s_addr,client_IP,sizeof(client_IP));
ntohs(clint_addr.sin_port);

client端实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//client的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERV_PORT 9726

/*错误处理函数*/
void sys_err(const char* str){
perror(str);
exit(1);
}

int main(){
int client_fd=0;
int ret=0;
int num=0;
int cnt=10;
char buf[BUFSIZ];

//connect的参数2填入服务器的文件描述符!
struct sockaddr_in serv_addr;
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERV_PORT);

/*点分十进制->网络二进制*/
/*协议族,源数据,目的数据(int32足够,因为IP地址就是32位)*/
inet_pton(AF_INET,"127.0.0.1",(void*)&serv_addr.sin_addr.s_addr);

/*客户端直接创建用于连接的套接字即可*/
client_fd=socket(AF_INET,SOCK_STREAM,0);
if(client_fd==-1)
sys_err("socket error");

/*将客户端套接字与服务器地址结构连接起来*/
ret=connect(client_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret!=0)
sys_err("connect error");

//业务逻辑
while(--cnt){
write(client_fd,"fuckyou\n",8);
num=read(client_fd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,num);
sleep(1);
}
close(client_fd);
return 0;
}

注意:套接字: 一个fd可以索引读写两个缓冲区

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

客户端和服务器启动后可以使用netstat命令查看链接情况:

netstat -apn | grep 9726

出错处理

为使错误处理的代码不影响主程序的可读性,把与socket相关的一些系统函数加上错误处理代码封装成新的函数,做成一个模块wrap.c。

封装的目的:在server.c编译过程中突出逻辑,将出错处理与逻辑分开。将原函数首字母大写进行错误处理,这样还可以跳转到原函数的manPage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//wrap.h
//存放网络通信相关常用自定义函数(声明)
//在server.c 和client.c中调用自定义函数
//联合编译server.c和wrap.c生成server
//联合编译client.c和wrap.c生成client
#ifndef __WRAP_H_
#define __WRAP_H_
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
// wrap.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
void perr_exit(const char *s)
{
perror(s);
exit(1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if ( (n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR))
goto again;
else
perr_exit("accept error");
}
return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = bind(fd, sa, salen)) < 0)
perr_exit("bind error");
return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = connect(fd, sa, salen)) < 0)
perr_exit("connect error");
return n;
}
int Listen(int fd, int backlog)
{
int n;
if ((n = listen(fd, backlog)) < 0)
perr_exit("listen error");
return n;
}
int Socket(int family, int type, int protocol)
{
int n;
if ( (n = socket(family, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = read(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
int Close(int fd)
{
int n;
if ((n = close(fd)) == -1)
perr_exit("close error");
return n;
}
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char *ptr;

ptr = vptr;
nleft = n;

while (nleft > 0) {
if ( (nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;

ptr = vptr;
nleft = n;

while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}

static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];

if (read_cnt <= 0) {
again:
if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return -1;
} else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}

ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;

for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
*ptr = 0;
return n - 1;
} else
return -1;
}
*ptr = 0;
return n;
}

半关闭

由原来的双工通信变为了单工通信, 客户端只能接受数据(缓冲区中的数据)

实现原理:

关闭了客户端套接字的写缓冲区

  • 之所以半关闭后Client仍能向Server发送ACK数据包, 是因为Client关闭的只是写缓冲, 连接还在

  • 连接在内核层面, 写缓冲在用户层面

  • 如果Server没有收到Client最后发来的ACK数据包, 它会一直发送FIN数据包, 直到Client回执为止

socket模型创建

socket模型创建

  • socket()—创建一个套接字, 用fd或文件句柄索引

  • bind()—绑定IP和port

  • listen()—设置监听上限(同时与Server建立连接数)

  • accpet()—阻塞监听客户端连接(传入一个上面创建的套接字, 传出一个连接的套接字)

  • 在客户端中的connect()中绑定IP和port, 并建立连接

socket函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数详解:
domain:
AF_INET 大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用

type:
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序

protocol:
0 表示使用默认协议。

返回值:
成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno

socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1 对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol指定为0即可。

bind函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd:
socket文件描述符

addr:
构造出IP地址加端口号

addrlen:
sizeof(addr)长度

返回值:
成功返回0,失败返回-1, 设置errno

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。

bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:

1
2
3
4
5
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET; //addr.family应该与sofkfd的domain保持一致;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666); //端口号为short类型(16bit)

首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。

listen函数

1
2
3
4
5
6
7
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:
socket文件描述符
backlog:
排队建立3次握手队列和刚刚建立3次握手队列的链接数和

查看系统默认backlog:cat /proc/sys/net/ipv4/tcp_max_syn_backlog

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

connect函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <sys/types.h> 					
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
socket文件描述符

addr:
传入参数,指定服务器端地址信息,含IP地址和端口号

addrlen:
传入参数,传入sizeof(addr)大小

返回值:
成功返回0,失败返回-1,设置errno

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。

accept函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <sys/types.h> 		
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
socket文件描述符

addr:
传出参数,返回链接客户端地址信息,含IP地址和端口号

addrlen:
传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小

返回值:
成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno

三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。 addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。

服务器的结构一般如下:

1
2
3
4
5
6
7
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
......
close(connfd);
}

整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1。

套接字

在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
2
3
4
5
6
#include<arpa/inet.h>
//h表示host,n表示network,l表示32位长整数,s表示16位短整数。
uint32_t htonl(uint32_t hostlong); //主要针对IP(host to network)
uint16_t htons(uint16_t hostshort); //主要针对port
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

IP地址转换函数

由于如192.168.45.2的IP地址为点分十进制表示, 需要转化为uint32_t型, 有现成的函数(IPv4和IPv6都可以转换,函数接口是void *addrptr):

1
2
3
4
5
6
7
8
9
10
11
int inet_pton(int af,const char* src,void* dst);//p表示点分十进制的ip,n为网络上的二进制ip
//参数 af: AF_INET/AF_INET6
//src:传入参数, 待转换的点分十进制的IP地址
//dst:传出参数, 转换后符合网络字节序的IP地址
//返回值:成功返回1,若参2无效返回0(异常),失败返回-1

const char* inet_ntop(int af,const char* src,char* dst,socklen_t size);
//src:传入参数, 待转换的网络字节序的IP地址
//dst:传出参数, 转换后的点分十进制IP地址, 是一块缓冲区
//size指定了缓冲区的大小
//返回值:成功返回dst指针,失败返回NULL指针, 设置errorno

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结构体表示。

sockaddr地址结构

bind函数原型:

1
2
3
4
5
6
7
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);

/*struct sockaddr是早已废弃的数据结构,已不再使用,用新的需要强转*/
struct sockaddr_in addr;
int bind(int sockfd,(struct sockaddr*)&addr,size);

sockaddr_in相关定义:

1
2
3
4
5
6
7
8
9
/*相关结构体定义,在man 7 ip*/
struct sockaddr_in{
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr{
uint32_t s_addr;
};

初始化方法:

1
2
3
4
5
6
7
8
9
addr.sin_family=AF_INET/AF_INET6;
addr.sin_port=htons(9527); //端口号为short类型(16bit)

int dst;
inet_pton(AF_INET,"192.168.10.2",(void*)&dst);
addr.sin_addr.s_addr=dst;

/*或者采取下面的方法*/
addr.sin_addr.s_addr=htonl(INADDR_ANY) //取出系统中任意有效的IP地址

网络名词术语解析

路由(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字节