1. 基本知识

从应用的角度出发,协议可理解为“规则”,是数据传输和数据的解释的规则。

1.1 协议

1.典型的协议

  • 传输层:常见协议有TCP/UDP协议

  • 应用层:常见的协议有HTTP协议、FTP协议

  • 网络层:常见的协议有IP协议、ICMP协议、IGMP协议

  • 网络接口层:常见的协议有ARP协议、RARP协议

2.分层模型结构

  • OSI七层模型:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

  • TCP/IP四层模型:网络接口层(链路层)、网路层、传输层、应用层

3.网络传输流程

注意:数据没有封装之前,是不能在网络中传递的。

4.ARP协议

在网络通讯时,源主机的应用程序知道目的主机的IP地址和端口号,却不知道目的主机的硬件地址,而数据包首先是被网卡接收到再去处理上层协议的,如果接收到的数据包的硬件地址与本机不符,则直接丢弃。因此在通讯前必须获得目的主机的硬件地址。ARP协议就起到这个作用,源主机发出ARP协议,询问IP地址是192.168.0.1的主机的硬件地址是多少,并将这个请求广播到本地网段,目的主机接收到广播ARP请求,发现其中的IP地址与本机相符,则发送一个ARP应答数据包给源主机,将自己的硬件地址填写在应答包中。

ARP协议:根据IP地址获取mac地址

以太网帧协议:根据mac地址,完成数据包传输

5.IP协议:

  • 版本:IPv4、IPv6

  • TTL:time to live。设置数据包在路由节点中的跳转上限,每经过一个路由节点,该值-1,减为0的路由,有义务将该数据包丢弃

  • 源IP/目的IP:32位—>4字节

补:IP地址:可以在网络环境中,唯一标识一台主机

端口号:可以网络的一台主机上,唯一标识一个进程

IP地址+端口号:可以在网络环境中,唯一标识一个进程

1.2 C/S模型与B/S模型

1.C/S模型(client-server)

  • 优点:缓存大量数据、协议选择灵活、速度快

  • 缺点:安全性低、不能跨平台、开发工作量较大

2.B/S模型

  • 优点:安全性高、跨平台、开发工作量较小

  • 缺点:不能缓存大量数据、严格遵守HTTP

2. 网络

2.1 网络套接字socket

一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)。在通信过程中,套接字一定是成对出现的。

1.字节序

  • 小端法(pc本地地址):高位存高位地址,低位存低位地址

  • 大端法(网络存储):高位存低位地址,低位存高位地址

2.转换函数

  • htonl:本地(IP) ——–> 网络(IP)

  • htons:本地(port) ——-> 网络(port)

  • ntohl:网络(IP) ——–> 本地(IP)

  • ntohs:网络(port) ——-> 本地(port)

3.IP地址转换函数

本地字节序(string IP) ——-> 网络字节序

int inet_pton(int af , const char *src , void *dst);

  • 参1:AF_INET、AF_INET6

  • 参2:传入参数,IP地址(点分十进制)

  • 参3:传出参数,转换后的网络字节序的IP地址

  • 返回值:成功(1);异常(0,说明src指向的不是一个有效的ip地址);失败(-1)

网络字节序—>本地字节序(string IP)

const char *inet_ntop(int af , const void *src , char *dst , socklen_t size);

  • 参1:AF_INET、AF_INET6

  • 参2:网络字节序IP地址

  • 参3:本地字节序(string IP)

  • 参4:参3的大小

  • 返回值:成功(参3);失败(NULL);

4.sockaddr地址结构(查看:man 7 ip)

1
2
3
4
5
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6
addr.sin_port = htons(9527); //端口号
addr.sin_addr.s_addr = htonl(INADDR_ANY); //取出系统中有效的任意IP地址(获取默认本地IP地址),二进制类型
bind(fd,(struct sockaddr *)&addr,size); //进行绑定,需要转换成sockaddr

2.2 网络相关函数

1.创建一个套接字

头文件:#include <sys/socket.h>

int socket(int domain , int type , int protocol);

  • 参1:AF_INET(IPV4)、AF_INET6(IPV6)、AF_UNIX(本地套接字)

  • 参2:SOCK_STREAM(流式协议)、SOCK_DGRAM(报式协议)

  • 参3:0即可

  • 返回值:成功(新套接字所对应文件描述符);失败(-1)

2.给socket绑定一个地址结构(IP+port)

头文件:include <arpa/inet.h>

int bind(int sockfd , const struct sockaddr *addr , socklen_t addrlen);

  • 参1:socket函数返回值

  • 参2:传入参数,自己的地址结构(IP+port)

  • 参3:地址结构的大小

  • 返回值:成功(0),失败(-1)

3.设置同时与服务器建立连接的上限数(同时进行三次握手的客户端数量)

int listen(int socket , int backlog);

  • 参1:socket函数返回值

  • 参2:上限数值。最大值为128

  • 返回值:成功(0);失败(-1)

4.阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符

int accept(int sockfd , struct sockaddr *addr , socklen_t *addrlen);

  • 参1:socket函数返回值

  • 参2:传出参数,得到成功与服务器建立连接的那个客户端的地址结构

  • 参3:传入传出参数,传入参2的大小,传出客户端地址结构实际大小

  • 返回值:成功(能与服务器进行数据通信的socket对应的文件描述符);失败(-1)

5.使用现有的socket与服务器建立连接

int connect(int sockfd , const struct sockaddr *addr , socklen_t addrlen)

  • 参1:socket函数返回值

  • 参2:传入参数,服务器的地址结构

  • 参3:服务器的地址结构的大小

  • 返回值:成功(0);失败(-1)

补:如果不使用bind绑定客户端地址结构,系统采用“隐式绑定”。

服务端实现:

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
#define SERV_PORT 9527                         //固定一个端口
void sys_err(const char *str){ //打印错误信息函数
perror(str);
exit(1);
}
int main(){
int lfd = 0,cfd = 0;
struct sockaddr_in serv_addr,clit_addr;
char buf[BUFSIZ],client_IP[1024];
int ret ,i;
socklen_t clit_addr_len;
//服务端结点信息
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
lfd = socket(AF_INET,SOCK_STREAM,0); //得到一个监听描述符
if(lfd == -1){
sys_err("socket error");
}
bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); //与本地进行绑定
listen(lfd,128); //设置一次性最大监听数128
clit_addr_len = sizeof(clit_addr);
cfd = accept(lfd,(struct sockaddr *)&clit_addr,&clit_addr_len); //监听连接客户端,连接成功得到一个它们之间通信的描述符
if(cfd == -1){
sys_err("accept error");
}
//打印建立连接的客户端的信息
printf("client ip:%s port:%d\n",
inet_ntop(AF_INET,&clit_addr.sin_addr.s_addr,client_IP,sizeof(client_IP)),
ntohs(clit_addr.sin_port)); //打印客户端的地址结构信息
while(1){
ret = read(cfd,buf,sizeof(buf)); //将客户端发来的信息存到buf
write(STDOUT_FILENO,buf,ret); //打印在屏蔽上
for(i = 0; i < ret; i++){ //将得到的字母转为大写
buf[i] = toupper(buf[i]);
}
write(cfd,buf,ret); //发送给客户端
}
close(lfd); //关闭监听描述符
close(cfd); //关闭通信描述符
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
#define SERV_PORT 9527                 //定义一个要连接的服务端的端口
void sys_err(const char *str){ //打印错误信息
perror(str);
exit(1);
}
int main(){
int cfd;
int conter = 10;
char buf[BUFSIZ];
struct sockaddr_in serv_addr; //服务器地址结构
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT); //要连接服务端的端口
inet_pton(AF_INET,"192.168.88.93",&serv_addr.sin_addr.s_addr); //要连接服务端的ip地址
cfd = socket(AF_INET,SOCK_STREAM,0); //得到一个通信描述符
if(cfd == -1){
sys_err("socket error");
}
int ret = connect(cfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); //连接服务端
while(--conter){
write(cfd,"hello\n",6); //向服务端发送信息
ret = read(cfd,buf,sizeof(buf)); //读取服务端发来的信息,存到buf
write(STDOUT_FILENO,buf,ret); //将buf的信息打印到终端屏幕
sleep(1);
}
close(cfd); //关闭通信描述符
return 0;
}

2.3 三次握手和四次挥手

1.三次握手

  • 主动发起连接请求端:发送SYN标志位,请求建立连接。携带序号号、数据字节数(0)、滑动窗口大小win。

  • 被动接受连接请求端:发送ACK(应答)标志位,同时携带SYN请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小win。

  • 主动发起连接请求端:发送ACK标志位,应答服务器连接请求,携带确认序号。

2.四次挥手

  • 主动关闭连接请求端:发送FIN标志位

  • 被动关闭连接请求端:应答ACK标志位 —>半关闭完成

  • 被动关闭连接请求端:发送FIN标志位

  • 主动关闭连接请求端:应答ACK标志位 —>连接全部关闭

3.滑动窗口:发送给连接的对端,本端的缓冲区大小(实时),保证数据不会丢失。

4.read函数的返回值:

  • 大于0:实际读到的字节数

  • 等于0:已经读到结尾(对端已经关闭)

  • -1:应进一步判断errno的值

  • errno=EAGAIM or EWOULDBLOCK:设置了非阻塞方式读,但没有数据到

  • errno=EINTR:慢速系统调用被中断

  • errno=”其他情况”:异常

2.3 多进程多线程的并发服务器

案例:多进程并发服务器实现

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
//下面代码对网络的函数进行了封装,所以直接调用的是封装的函数
void catch_child(int signum){
while(waitpid(0,NULL,WNOHANG)>0);
return;
}
int main(){
int lfd = Socket(AF_INET,SOCK_STREAM,0); //得到一个监听的描述符
struct sockaddr_in srv_addr; //存服务端信息的结构体
bzero(&srv_addr,sizeof(srv_addr)); //将地址结构清0
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(9527); //定义端口
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //使用本地ip地址
Bind(lfd,(struct sockaddr *)&srv_addr,sizeof(srv_addr)); //绑定
Listen(lfd,128);
struct sockaddr_in clit_addr; //存放连接客户端信息的结构体
socklen_t clit_len;
clit_len = sizeof(clit_addr);
pid_t pid; //进程id
char buf[1024];
int i;
int cfd;
while(1){
cfd = Accept(lfd,(struct sockaddr *)&clit_addr,&clit_len); //监听连接
pid = fork(); //建立连接成功才创建子线程,这样父子进程使用的文件描述符底层是同一套
if(pid<0){
perror("fork error");
exit(-1);
}else if(pid==0){ //子线程负责通信
close(lfd); //父进程集合子进程都有lfd和cfd,在子进程中,lfd没有什么作用,可以直接关闭,因为子进程只完成任务,不需要监听连接
break; //跳出外面做,也可以全部写在这里面
}else{ //父进程负责回收线程
close(cfd); //在父进程中,父进程负责监听连接客户端,所以通信描述符cfd在父进程中没有用
struct sigaction act;
act.sa_handler = catch_child; //捕抓函数
sigemptyset(&act.sa_mask); //将阻塞信号集置为0
act.sa_flags = 0;
int ret = sigaction(SIGCHLD,&act,NULL); //当子进程状态发送变化,就触发捕捉函数
if(ret!=0){
perror("sigaction error");
exit(-1);
}
continue;
}
}
if(pid==0){ //下面是子进程负责通信
for(;;){
int ret = read(cfd,buf,sizeof(buf)); //读取客户端发来的信息
if(ret == 0){ //说明客户端那边请求断开
close(cfd);
exit(1);
}
for(i=0;i<ret;i++){
buf[i]=toupper(buf[i]); //小写转大写
}
write(cfd,buf,ret); //将转换好的发送给客户端
write(STDOUT_FILENO,buf,ret); //将转换好的发送到屏幕
}
}
return 0;
}

2.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
//下面代码对网络的函数进行了封装,所以直接调用的是封装的函数
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[1024];
char clie_ip[16];
while(1){
n = read(ts->connfd,buf,1024); //读客户端发送来的信息存到buf
if(n == 0){
printf("the client %d closed ....\n",ts->connfd);
break;
}
//打印客户端的信息
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET,&(*ts).cliaddr.sin_addr.s_addr,clie_ip,sizeof(clie_ip)),
ntohs((*ts).cliaddr.sin_port)); //打印客户端信息
for(i=0;i<n;i++){
buf[i] = toupper(buf[i]); //小写转大写
}
write(STDOUT_FILENO,buf,n); //写到屏幕上
write(ts->connfd,buf,n); //写给客户端
}
close(ts->connfd); //跳出循环,说明客户端端口了连接,关闭于客户端的通信描述符
return (void *)0; //函数返回,相当于子线程结束,这里等价与pthread_exit(0);
}
int main(){
int listenfd = Socket(AF_INET,SOCK_STREAM,0); //得到监听描述符
struct sockaddr_in servaddr; //存放服务端信息结构体
bzero(&servaddr,sizeof(servaddr)); //对结构体清0
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9527); //指定端口号
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //指定本地任意IP
Bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)); //绑定
Listen(listenfd,128);
printf("accept client connect ....\n");
struct sockaddr_in cliaddr; //存放客户端信息结构体
socklen_t cliaddr_len;
struct s_info ts[256]; //定义一个结构体数组,用来存放每一个子线程信息
int i=0;
pthread_t tid; //定义一个线程变量
while(1){
cliaddr_len = sizeof(cliaddr);
int connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&cliaddr_len); //阻塞监听客户端连接请求
ts[i].cliaddr = cliaddr; //将客户端结构体信息存放在管理结构体中
ts[i].connfd = connfd; //将与客户端通信描述符存放在管理结构体中
pthread_create(&tid,NULL,do_work,(void *)&ts[i]); //创建线程,执行回调函数
pthread_detach(tid); //分离子线程,父线程不会阻塞在这里等待子线程结束,而是由内核释放子线程
i++;
}
return 0;
}

2.5 TCP状态转换

1.查看网络状态

  • netstat -apn

  • netstat -apn | grep client ———-> 查看对应进程(client也可以写成对应端口号)

2.TCP状态转换图

  • 主动发送请求连接端:CLOSED —– 发送SYN —– SYN_SEND —– 接收ACK、SYN —– SYN_SEND —– 发送ACK —– ESTABLISHED(数据通信态)

  • 主动关闭请求连接端:ESTABLISHED(数据通信态) —– 发送FIN —– FIN_WAIT_1 —– 接收ACK —– FIN_WAIT_2(半关闭) —– 接收对端发送FIN —– FIN_WAIT_2(半关闭) —– 回发ACK —– TIME_WAIT(只有主动关闭连接方,会经历该状态) —– 等2MSL时长(40秒) —–CLOSED

  • 被动接收连接请求端:CLOSK —– LISTEN —– 接收SYN —– LISTEN —– 发送ACK、SYN(对方收到) —– SYN_RCVD —– 接收ACK —– ESTABLISHED(数据通信态)

  • 被动关闭连接请求端:ESTABLISHED(数据通信态)—–接收FIN —– ESTABLISHED —– 发送ACK —– CLOSE_WAIT(说明主动连接端处于半关闭状态) —– 发送FIN —– LAST_ACK —– 接收ACK —– CLOSED

1_5

注意:如果是客户端先关闭,服务端后关闭,则客户端会TIME_WAIT,服务端不会,所以又可以马上对服务端进行开启;如果是服务端先关闭,客户端后关闭,则服务端会进入TIME_WAIT状态,此时还没有进入完全关闭状态,会占用端口40秒,所以你这段时间内又开服务端会失败,因为端口号还被占用着。

补:2 MSL时长:一定出现在主动关闭连接请求端(TIME_WAIT);保证最后一个ACK能成功被对端接收(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求)。

3.端口复用

在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为TCP连接没有完全断开指的是connfd没有完全断开,而我们重新的是listenfd,虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。

在server.c代码的socket()和bind()调用之间插入如下代码即可:

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

4.半关闭

通信双方中,只有一端关闭通信。———> FIN_WAIT_2

close(fd);

shutdown(int fd,int how);

  • 参2:SHUT_RD(关读端);SHUT_WR(关写端);SHUT_RDWR(关读写端);

注意:shutdown在关闭多个文件描述符应用的文件时(dup2重定向,多个文件描述符指向一个文件),采用全关闭方法(关闭一个相当于关闭了全部);close只关闭一个文件描述符,其他指向该文件的文件描述符还可以使用

3. 多路IO转接服务器

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

服务端accept客户端的三种方式:阻塞、非阻塞忙轮询、响应式(多路IO转接)

3.1 select多路IO转接

原理:借助内核,select来监听客户端连接、数据通信事件。

1.清空一个文件描述符集合

  • void FD_ZERO(fd_set *set);
    • 使用:fd_set rset; FD_ZERO(&rset);

2.将待监听的文件描述符,添加到监听集合中

  • void FD_SET(int fd, fd_set *set);
    • 使用:FD_SET(3,&rset); FD_SET(5,&rset); FD_SET(6,&rset);

3.将一个文件描述符从监听集合中移除

  • void FD_CLR(int fd, fd_set *set);
    • 使用:FD_CLR(4, &rset);

4.判断一个文件描述符是否在监听集合中

  • int FD_ISSET(int fd, fd_set *set);

    • 使用:FD_ISSET(4, &rset);

    • +返回值:在(1);不在(0);

5.select监听

  • int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

    • 参1:监听的所有文件描述符中,最大文件描述符+1;

    • 参2:读文件描述符监听集合(传入传出参数);

    • 参3:写文件描述符监听集合(传入传出参数); —>不用就NULL

    • 参4:异常文件描述符监听集合(传入传出参数); —>不用就NULL

    • 参5:大于0(设置监听超时时长);NULL(阻塞监听);0(非阻塞监听,轮询);

    • 返回值:

      • 大于0:所有监听集合(3个)中,即读集合、写集合、异常集合,满足对应事件的总数

      • 等于0:没有满足监听条件的文件描述符

      • 等于-1:errno

6.select优缺点:

  • 缺点:(a)监听上限受文件描述符限制,最大1024;(b)检测满足条件的fd,只有自己添加业务逻辑提高效率,提高了编码难度

  • 优点:跨平台。Windows、linux、macOS、Unix、类Unix、mips

案例:使用select完成服务端的实现

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
//下面代码对网络的函数进行了封装,所以直接调用的是封装的函数
int main(){
int listenfd = Socket(AF_INET,SOCK_STREAM,0); //得到监听连接的文件描述符
int opt = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //实现端口复用
struct sockaddr_in serv_addr; //定义一个存放服务端信息的结构体
bzero(&serv_addr,sizeof(serv_addr)); //将该结构体清0
//初始化服务端结构体的信息
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //使用本地ip
serv_addr.sin_port = htons(9527); //端口使用9527
Bind(listenfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); //服务端绑定
Listen(listenfd,128);
fd_set rset,allset; //定义读集合,备份集合allset
int ret;
int i,j,n;
char buf[1024];
struct sockaddr_in clie_addr; //定义一个存放客户端信息的结构体
socklen_t clie_addr_len;
int connfd;
int maxfd = listenfd; //将最大fd定义伪监听的fd(刚开始)
FD_ZERO(&allset); //清空监听集合
FD_SET(listenfd,&allset); //将监听fd添加到监听集合中
while(1){
rset = allset; //备份需要监听的fd集合
//使用select监听,得到的传出rset集合里面都是就绪的文件描述符(需要处理的),得到的ret是该集合的个数(满足处理的个数)
ret = select(maxfd+1,&rset,NULL,NULL,NULL);
if(ret<0){
perror("select error");
exit(-1);
}
if(FD_ISSET(listenfd,&rset)){ //判断listenfd监听描述符是否满足监听的读事件
//如果满足,说明有客户端发送来了请求连接
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd,(struct sockaddr *)&clie_addr,&clie_addr_len); //建立连接,不会阻塞
printf("创建连接成功connfd = %d\n",connfd);
FD_SET(connfd,&allset); //将新产生的通信文件描述符fd添加到监听集合中(下一论监听)
if(maxfd<connfd){ //修改最大文件描述符
maxfd = connfd;
}
if(ret==1){ //说明select只返回一个,并且是listenfd,后续执行无须执行
continue;
}
}
for(i=listenfd+1;i<=maxfd;i++){ //处理满足读事件的通信文件描述符
if(FD_ISSET(i,&rset)){ //找到满足读事件的那个通信文件描述符
n = read(i,buf,sizeof(buf)); //接收它发来的数据
if(n==0){ //检测到客户端是否已经关闭连接
close(i);
FD_CLR(i,&allset); //将关闭的fd移除监听集合
}else if(n==-1){
perror("read error"); //处于异常
}
for(j=0;j<n;j++){
buf[j] = toupper(buf[j]); //小写转大写
}
write(i,buf,n); //通过通信描述符,发送处理好的信息给客户端
write(STDOUT_FILENO,buf,n); //将处理的信息打印到终端
}
}
}
close(listenfd); //关闭监听描述符
}

3.2 poll多路IO转接

1.poll监听函数

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

  • 参1:监听的文件描述符(数组)

    1
    2
    3
    4
    5
    struct pollfd{
    int fd; //待监听的文件描述
    short events; //监听对应的事件(POLLIN、POLLOUT、POLLERR)
    short revnets; //传入时给0,则返回满足对应事件POLLIN、//POLLOUT、POLLERR
    }
  • 参2:监听结构体数组的实际有效监听个数(刚开始为1,只有监听描述符)

  • 参3:大于0(超时时长,单位:毫秒);等于-1(阻塞等待);等于0(不阻塞)。

  • 返回值:返回满足对应监听事件的文件描述符总个数

2.poll的优缺点

  • 优点:自带数组结构,可以将监听事件集合和返回事件集合分离;拓展监听上限;可以突破1024。

  • 缺点:不能跨平台,只能 inux系统;无法直接定位满足监听事件的文件描述符,编码难度较大。

3.突破1024文件描述符限制(对select不管用)

  • cat /proc/sys/fs/file-max ——->当前计算机所能打开的最大文件个数,受硬件影响

  • ulimit -a ——–>查看当前用户下的进程默认打开文件描述符个数(不改为1024)

修改:打开vim /etc/security/limits.conf

写入:

  • soft nofile 65536 ———->设置默认值,可以直接借助命令修改
  • hard nofile 100000 ———–>命令修改上限

修改后需要注销用户,使其生效,相当于重新启动

案例:使用poll实现多路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
//下面代码对网络的函数进行了封装,所以直接调用的是封装的函数
int main(){
int listenfd = Socket(AF_INET,SOCK_STREAM,0); //得到监听描述符
int opt = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //端口复用
struct sockaddr_in serv_addr; //存放服务端信息的结构体
bzero(&serv_addr,sizeof(serv_addr)); //清0
//初始化服务端的结构体信息
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(9527);
Bind(listenfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); //服务端绑定
Listen(listenfd,128);
struct pollfd client[1024]; //定义一个文件描述符的结构体(还包含其他内容)
client[0].fd = listenfd; //要监听的第一个文件描述符存入client[0]位置
client[0].events = POLLIN; //监听listenfd文件描述符的读事件
int i;
for(i=1;i<1024;i++){ //因为listenfd文件描述符已经占了0位置,所以从1位置开始
client[i].fd = -1; //初始化文件描述符结构体数组,fd=-1意味着该位置未被占用
}
int maxi = 0; //初始化client[]数组有效元素中最大元素下标(刚开始只有listenfd文件描述符结构体)
int nready;
struct sockaddr_in cliaddr; //存放客户端信息的结构体
socklen_t clilen;
char client_ip[16]; //客户端的ip地址(一般在要打印客户端信息时,才会用到)
int connfd;
for(;;){
nready = poll(client,maxi+1,-1); //监听是否有客户端连接请求。结构体数组;有效文件描述符个数;阻塞等待
if(client[0].revents & POLLIN){ //为真,说明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.s_addr,client_ip,sizeof(client_ip)),
ntohs(cliaddr.sin_port));
int i;
for(i=1;i<1024;i++){
if(client[i].fd<0){
client[i].fd = connfd; //找到client[]中空闲的位置,存放通信描述符connfd
break; //找到后直接跳出循环
}
}
if(i==1024){ //检测client是否满了
perror("too many clients");
exit(-1);
}
client[i].events = POLLIN; //将刚刚得到的通信文件描述符设为检测读事件
if(i>maxi){
maxi = i; //设置client文件描述符结构体数组的最大有效下标
}
if(--nready<=0){ //当只有一个就绪事件时,这个事件必为监听文件描述符的,所以就没有通信描述符的就绪事件,就不用执行下面,继续回到poll
continue;
}
}
//开始通信文件描述符
int sockfd;
int i,j,n;
char buf[1024];
for(i=1;i<=maxi;i++){ //前面的if没有满足,说明有通信文件描述符满足读事件就绪,检测client[],看是哪个connfd就绪
if((sockfd = client[i].fd)<0){ //从通信文件描述符第一个找到最后,如果是<0,说明对应通信描述符没有收到客户端的信息,
continue;
}
if(client[i].revents & POLLIN){
if((n=read(sockfd,buf,1024))<0){
if(errno==ECONNRESET){ //收到RST标志
printf("client[%d] aborted connection\n",i);
}else{ //还有其他情况的错误,这里就统一这种了,没有一一列出来
perror("read error");
}
close(sockfd);
client[i].fd = -1; //poll中不监控该文件描述符,直接置为-1即可,不用像select中那样移除
}else if(n == 0){
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]); //小写转大写
}
write(sockfd,buf,n);
write(STDOUT_FILENO,buf,n);
}
if(--nready<=0){ //每次执行完通信描述符,都--了,当为0的时候,说明就没有了,直接跳出循环,不用再找了
break;
}
}
}
}
return 0;
}

3.3 epoll多路IO转接

1.int epoll_create(int size);

  • 参数:创建的红黑树的监听节点数量。(仅供内核参考)

  • 返回值:成功(指向新创建的红黑树的根节点的fd);失败(-1,errno)

2.int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

  • 参1:epoll_create函数的返回值

  • 参2:对该监听红黑树所做的操作

    • EPOLL_CTL_ADD:添加fd到监听红黑树
    • EPOLL_CTL_MOD:修改fd在监听红黑树上的监听事件
    • EPOLL_CTL_DEL:将一个fd从监听红黑树上摘下(取消监听)
  • 参3:待监听的fd

  • 参4:本质是struct epoll_event结构体地址。结构体内有events包括EPOLLIN、EPOLLOUT、EPOLLERR;有data,它是共用体,包括fd,(void *)ptr等。

3.int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

  • 参1:epoll_create函数的返回值

  • 参2:传出参数,是数组,满足监听条件的哪些fd结构体

  • 参3:数组元素的总个数

  • 参4:等于-1(阻塞);等于0(不阻塞);大于0:超时时间(毫秒)

  • 返回值:大于0(满足监听的总个数,可以用作循环上限);等于0(没有fd满足监听事件);等于-1(失败,errno);

案例:使用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
//下面代码对网络的函数进行了封装,所以直接调用的是封装的函数
int main(){
int listenfd = Socket(AF_INET,SOCK_STREAM,0); //得到监听描述符
printf("listenfd = %d\n",listenfd);
int opt = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //端口复用
struct sockaddr_in serv_addr; //存放服务端信息的结构体
bzero(&serv_addr,sizeof(serv_addr)); //清0
//初始化服务端结构体信息
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(9527);
Bind(listenfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); //绑定
Listen(listenfd,128);
int efd = epoll_create(39); //创建epoll模型,efd指向红黑树的根节点。参数为设定又几个红黑树(大于0即可)
printf("efd = %d\n",efd);
if(efd == -1){
perror("epoll_create");
exit(0);
}
struct epoll_event tep,ep[1024]; //tep为epoll_ctl的参数,ep[]为epoll_wait的参数
tep.events = EPOLLIN; //读监听事件
tep.data.fd = listenfd; //指定了listenfd的监听事件为读
int res = epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep); //epoll_create返回值;是添加(选项);添加的描述符;添加的ifd所对应的事件(结构体类型);
if(res == -1){
perror("epoll_ctl");
exit(0);
}
struct sockaddr_in cliaddr; //装客户端信息的结构体
socklen_t clilen;
char buf[1024];
int num = 0;
for(;;){
//epoll为server阻塞监听事件,ep为struct epoll_event类型数组(传出参数,返回满足监听的那些描述符);参数3为数组容量;-1表示永久阻塞
int nready = epoll_wait(efd,ep,1024,-1); //返回的是所有就绪的描述符的个数,与ep数组里面的个数一样
if(nready == -1){
perror("epoll_wait error");
exit(0);
}
int i;
for(i=0;i<nready;i++){
if(ep[i].data.fd == listenfd){ //如果成立,说明是监听事件
clilen = sizeof(cliaddr);
char client_ip[16];
int connfd = Accept(listenfd,(struct sockaddr *)&cliaddr,&clilen); //与客户端建立连接
printf("received from %s at PORT %d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,client_ip,sizeof(client_ip)),ntohs(cliaddr.sin_port));
printf("cfd %d----client %d\n",connfd,++num);
tep.events = EPOLLIN; //往红黑树上添加新的结点
tep.data.fd = connfd;
res = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep); //将新产生的通信描述符加入到红黑树
if(res == -1){
perror("epoll_ctl_error");
exit(0);
}
}else{ //如果不是监听描述符就绪,就是通信描述符
int sockfd = ep[i].data.fd;
int n = read(sockfd,buf,1024); //读取从客户端发来的信息
if(n == 0){ //客户端请求断开
res = epoll_ctl(efd,EPOLL_CTL_DEL,sockfd,NULL); //将该文件描述符从红黑树摘除
if(res == -1){
perror("epoll_ctl error");
exit(-1);
}
close(sockfd);
printf("client[%d] closed connection\n",sockfd);
}else if(n<0){ //出现异常
perror("read n<0 error:");
res = epoll_ctl(efd,EPOLL_CTL_DEL,sockfd,NULL); //摘除节点
close(sockfd);
}else{
int j;
for(j=0;j<n;j++){
buf[j] = toupper(buf[j]); //转大写
}
write(STDOUT_FILENO,buf,n); //将处理的信息打印到终端
write(sockfd,buf,n); //将处理的信息发送给客户端
}
}
}
}
close(listenfd); //关闭监听描述符
close(efd); //关闭红黑树的根结点
return 0;
}

4.epoll事件模式

ET模式:边沿触发,缓冲区剩余未读尽的数据不会导致epoll_wait返回。新的事件满足,才会触发。

FT模式:水平触发(默认模式),缓冲区剩余未读尽的数据会导致epoll_wait返回。

注意:像select和epoll它们监听的都是文件描述符,不是套接字,所以可以结合管道来使用。

比较:FT是缺省的工作方式,并且同时支持阻塞和非阻塞;ET是高速工作方式,只支持非阻塞(所以ET常与非阻塞一起用)–>非阻塞通常搭配忙轮询。

结论:epoll的ET模式,高效模式,但是只支持非阻塞模式(no black)

1
2
3
4
5
6
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&event);
int flag = fcntl(cfd,F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd,F_SETFL,flg);

5.epoll的优缺点

  • epoll优点:高效,突破1024文件描述符

  • epoll缺点:不能跨平台,只能linux系统

案例:通过epoll和管道测试ET和FT模式

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
int main(){
int efd,i;
int pfd[2];
pid_t pid; //定义一个进程变量
char buf[MAXLINE],ch = 'a';
pipe(pfd); //创建一个管道
pid = fork(); //创建一个子进程
if(pid==0){ //子进程负责写端
close(pfd[0]); //关闭读端
while(1){
for(i=0;i<MAXLINE/2;i++){
buf[i] = ch;
} //这时为aaaaa
buf[i-1] = '\n'; //为aaaa\n
ch++; //ch变为b
for(;i<MAXLINE;i++){ //i从5开始继续循环
buf[i] = ch;
} //这时为aaaa\nbbbbb
buf[i-1] = '\n'; //为aaaa\nbbbb\n
ch++; //ch变为c
write(pfd[1],buf,sizeof(buf)); //将buf里面的内容写到管道的写端
sleep(5); //睡眠5秒后,继续上面循环
}
close(pfd[1]); //执行完后,关闭写端
}else if(pid>0){ //父进程负责读
struct epoll_event event; //定义一个epoll_event的结构体
struct epoll_event resevent[10]; //定义一个epoll_event的结构体数组
int res ,len;
close(pfd[1]); //关闭写端
efd = epoll_create(10); //efd也是文件描述符,创建一个容纳10个节点的红黑树
//event.events = EPOLLIN | EPOLLET; //ET边沿触发:未读完的数据不会再次提醒,除非有新数据写入
event.events = EPOLLIN; //水平触发(默认)。这种模式下,没有读完的数据,马上又会进行读
event.data.fd = pfd[0]; //将fd定义为管道的读端
epoll_ctl(efd,EPOLL_CTL_ADD,pfd[0],&event); //将管道的读端挂到红黑树上
while(1){
res = epoll_wait(efd,resevent,10,-1);
printf("res %d\n",res); //打印满足就绪的文件描述符结构
if(resevent[0].data.fd == pfd[0]){ //如果是读端满足就绪
len = read(pfd[0],buf,MAXLINE/2); //从管道的读端读一半数据到buf,还有一半数据留在了管道
write(STDOUT_FILENO,buf,len); //将读到的内容写到屏幕上
}
}
close(pfd[0]); //关闭管道的读端
close(efd); //关闭红黑树的根节点
}else{
perror("fork");
exit(-1);
}
return 0;
}

4. 其它通信方式

4.1 TCP通信和UDP通信各自的优缺点

TCP:面向连接的,可靠数据报传输。对于不稳定的网络层,采取完全弥补的通信方式。丢包重传。

  • 优点:数据流量稳定、传输速度稳定、顺序稳定

  • 缺点:传输速度慢、效率低、资源开销大

使用场景:数据的完整型要求较高,不追求效率。(大数据传输、文件传输)

2.UDP:无连接的,不可靠的数据报传递。对于不稳定的网络层,采取完全不弥补的通信方式。默认还原网络状况。

  • 优点:传输速度快、效率高、资源开销小

  • 缺点:数据流量不稳定、传输速度不稳定、顺序不稳定

使用场景:对时效性要求较高的场合,稳定性其次。(游戏、视频会议、视频电话)—–>腾讯、华为、阿里:应用层数据校验协议,弥补UDP的不足。

4.2 UDP通信

1.UDP中负责读数据函数

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

  • 参1:套接字

  • 参2:缓冲区地址

  • 参3:缓冲区大小

  • 参4:0

  • 参5:传出参数,对端的地址结构

  • 参6:传入传出

  • 返回值:成功(接收数据字节数);失败:-1(errno),0(对端关闭)

2.UDP中负责写数据函数

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

  • 参1:套接字

  • 参2:存储数据的缓冲区

  • 参3:数据长度

  • 参4:0

  • 参5:传入参数,目标地址结构

  • 参6:地址结构长度

  • 返回值:成功(写出数据字节数);失败:-1(errno)

UDP服务端实现

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
#define BUFSIZ 1024
int main(){
int sockfd = socket(AF_INET,SOCK_DGRAM,0); //第2个参数表示是报式协议
struct sockaddr_in serv_addr; //存放服务端数据的结点
bzero(&serv_addr,sizeof(serv_addr)); //清0
//初始化服务端结构体
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(9527);
bind(sockfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)); //绑定
struct sockaddr_in clie_addr; //存放客户端数据结构体
socklen_t clie_addr_len;
int n;
char buf[BUFSIZ];
char str_ip[16];
printf("Accepting connections.....\n");
while(1){
clie_addr_len = sizeof(clie_addr);
n = recvfrom(sockfd,buf,BUFSIZ,0,(struct sockaddr *)&clie_addr,&clie_addr_len); //接收客户端传来的信息,得到客户端的地址结构信息
if(n == -1){
perror("recvfrom error");
}
printf("received from %s at port %d\n",
inet_ntop(AF_INET,&clie_addr.sin_addr.s_addr,str_ip,sizeof(str_ip)),
ntohs(clie_addr.sin_port));
int i;
for(i=0;i<n;i++){
buf[i] = toupper(buf[i]); //小写转大写
}
n = sendto(sockfd,buf,n,0,(struct sockaddr *)&clie_addr,sizeof(clie_addr)); //向客户端发送数据
if(n == -1){
perror("sendto error");
}
}
close(sockfd); //关闭文件描述符
return 0;
}

UDP客户端实现

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
#define BUFSIZ 1024
int main(){
int sockfd = socket(AF_INET,SOCK_DGRAM,0); //第2个参数表示是报式协议
struct sockaddr_in servaddr; //存放服务端信息的结构体
bzero(&servaddr,sizeof(servaddr)); //清0
//初始化服务端信息
servaddr.sin_family = AF_INET;
inet_pton(AF_INET,"192.168.88.93",&servaddr.sin_addr);
servaddr.sin_port = htons(9527);
int n;
char buf[BUFSIZ];
while(fgets(buf,BUFSIZ,stdin) != NULL){ //从键盘输入存到buf中
n = sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&servaddr,sizeof(servaddr)); //将buf里的信息发送给服务端,地址结构信息是服务端的
if(n == -1){
perror("sendto error");
}
n = recvfrom(sockfd,buf,BUFSIZ,0,NULL,0); //接收服务端发来的信息,NULL:表示不关心对端信息
if(n == -1){
perror("recvfrom error");
}
write(STDOUT_FILENO,buf,n); //将服务端写来的信息(buf里面)打印到屏幕
}
close(sockfd);
return 0;
}

4.3 本地套接字(domain)

本地套接字与网络套接字使用的区别:

  • 网络套接字中用的AF_INET;本地套接字是用AF_UNIX/AF_LOCAL

  • 网络套接字中的地址结构是sockaddr_in;本地套接字是用sockaddr_un

  • 网络套接字中是绑定ip和端口,本地套接字是绑定socket文件即可(伪文件),类似于管道(但不是)

补:本地套接字只能用于本机的两个进程通信。稳定性强,双向全双工。

注意:bind()函数调用成功,会创建一个socket,因此为保证bind成功,通常我们在bind之前,可以使用unlink(“serv.socket”);本地套接字中的客户端不能依赖隐式绑定,需要自己创建一个socke文件后绑定。

本地套接字的服务端实现:

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
#define SERV_ADDR "serv.socket"       //服务端用到的socetk文件
int main(){
int lfd = Socket(AF_UNIX,SOCK_STREAM,0); //创建本地套接字的第一个参数是AF_UNIX;第二个参数是报式还是流式都可以,返回一个本地套接字
struct sockaddr_un servaddr; //存放服务端信息的结构体
bzero(&servaddr,sizeof(servaddr)); //清0
//初始化本地套接字的服务端信息
servaddr.sun_family = AF_UNIX;
strcpy(servaddr.sun_path,SERV_ADDR); //定义的文件名,相当于本地套接字的地址结构
int len = offsetof(struct sockaddr_un,sun_path) + strlen(servaddr.sun_path); //结构体大小等于2(AF_UNIX的大小)+文件名大小
unlink(SERV_ADDR); //确保bind之前serv.sock文件(伪文件)不存在当前目录(执行过程序后,会在当前目录下创建该文件,用于传数据),bind会创建该文件
Bind(lfd,(struct sockaddr *)&servaddr,len); //参3不能是sizeof(servaddr)
Listen(lfd,20);
printf("accept .....\n");
struct sockaddr_un cliaddr;
int size;
int i;
char buf[1024];
while(1){
len = sizeof(cliaddr);
int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,(socklen_t *)&len); //套接字;客户端地址结构(传出);传入传出
len -= offsetof(struct sockaddr_un,sun_path); //得到文件名大小(传出来的len-偏移位置(AF_UNIX)大小)
cliaddr.sun_path[len] = '\0'; //确保打印时没有乱码出现
printf("client bind filename %s\n",cliaddr.sun_path);
while((size = read(cfd,buf,sizeof(buf))) > 0){
for(i = 0; i < size; i++){
buf[i] = toupper(buf[i]);
}
write(cfd,buf,size);
}
close(cfd);
}
close(lfd);
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
#define SERV_ADDR "serv.socket"       //服务端用到的socetk文件
#define CLIE_ADDR "clie.socket" //客户端用到的socket文件
//本地套接字通信
int main(){
int cfd = Socket(AF_UNIX,SOCK_STREAM,0); //创建本地套接字的第一个参数是AF_UNIX;第二个参数是报式还是流式都可以,返回一个本地套接字
struct sockaddr_un cliaddr; //存放客户端信息的结构体
bzero(&cliaddr,sizeof(cliaddr)); //清0
//初始化客户端结构体信息
cliaddr.sun_family = AF_UNIX;
strcpy(cliaddr.sun_path,CLIE_ADDR); //定义的文件名,相当于本地套接字的地址结构
int len = offsetof(struct sockaddr_un,sun_path) + strlen(cliaddr.sun_path); //结构体大小等于2(AF_UNIX的大小)+文件名大小
unlink(CLIE_ADDR); //确保bind之前clie.sock文件(伪文件)不存在,bind会创建该文件
Bind(cfd,(struct sockaddr *)&cliaddr,len); //参3不能是sizeof(cliaddr)

struct sockaddr_un servaddr; //存到服务端信息的结构体
bzero(&servaddr,sizeof(servaddr));
servaddr.sun_family = AF_UNIX;
strcpy(servaddr.sun_path,SERV_ADDR); //这些是要连的服务端的信息
len = offsetof(struct sockaddr_un,sun_path) + strlen(servaddr.sun_path); //计算服务端地址结构的有效长度
Connect(cfd, (struct sockaddr *)&servaddr,len); //连接服务端
char buf[1024];
while(fgets(buf,sizeof(buf),stdin) != NULL){ //从输入端输入信息到buf
write(cfd,buf,strlen(buf)); //从buf将信息传给服务端
len = read(cfd,buf,sizeof(buf)); //从服务端得到的信息存在buf
write(STDOUT_FILENO,buf,len); //将buf里的信息写到屏幕
}
close(cfd);
return 0;
}