Linux网络编程是在Linux操作系统上进行的,允许开发人员编写能够进行网络通信的应用程序,如文件传输服务(FTP)、Web服务器、即时通讯工具等;这种编程广泛利用了Linux提供的网络接口和协议栈,使得应用程序可以和不同主机、不同程序进行信息交互,Linux网络编程主要包括使用套接字(Sockets)、系统调用、各种网络协议(著名的TCP/IP协议、UDP协议)来处理各种网络数据。

Socket编程

Socket简介

Socket也是进程间通信方式之一,1982年,Berkeley Software Distributions操作系统引入了socket作为本地进程间的通信接口,1986年,其又扩展了socket使其能够支持UNIX下的TCP/IP通信;socket是一个编程接口,返回一个特殊的文件描述符,使得网络通信方法就像操作本地文件(OPEN--WRITE/READ--CLOSE)一样简便,其可以兼容不同的网络协议,包括TCP/IP、UDP协议等著名协议。

Socket类型

流式套接字(SOCK_STREAM):提供面向连接、可靠的数据传输服务,数据无差错、无重复发送且发送顺序接收,具有流量控制策略,数据被看成字节流,没有长度控制;(如TCP协议)

数据报套接字(SOCK_DGRAM):提供无连接服务,数据包以独立的数据包形式被发送,不提供无差错保证,数据可能丢失或者重复,接收有可能是乱序的;(如UDP协议)

原始套接字(SOCK_RAW):可以对较低层次的协议进行直接访问;(如IP、ICMP协议)

端口

不同的主机程序需要实现通信,通过IP地址可以访问不同的主机,而主机上不同的程序需要通过端口号来区分;在Internet协议中,端口号是一个16位数值,为0到65535;分类如下: 1. 知名端口(Well-Known Ports,0-1023):这些端口号被互连网号码分配机构(IANA)分配给特定的服务,如HTTP使用80,HTTPS使用443等被系统占用; 2. 注册端口(Registered Ports,1024-49151):不是IANA严密控制的,但仍有记录避免重复,用户级的应用程序多使用这个范围的端口; 3. 动态或私有端口(Dynamic/Private Ports,49152-65535):这些端口对于普通的网络用户是不被控制的,可以由任何系统上的应用程序使用。它们通常用于客户端软件的临时通信端点,也称为临时端口(ephemeral ports)。

TCP协议和UDP协议的端口号是独立的,因此可以重复设置;对用户而言,最常使用的为5000~65535,一般为8888、9999这些容易记忆的端口号;如果需要查看哪些端口号被占用,可以通过

1
vim /etc/services

字节序(Byte Order)

字节序是计算机内存或者网络传输中,多字节数据如何按字节顺序进行存储的约定,例如计算机的最基本存储单位是字节(Byte)(注意:最小单位才是位),假设收到数据为0x12fd(16位数据),那么先存储12还是先存储fd就产生了顺序,主要有:

  1. 大端序:高位字节存放在内存的低地址,则低地址先存放12,高地址存放fd;
  2. 小端序:低位字节存放在内存的低地址,则低地址先存放fd,高地址存放12;

不同处理器架构可能采取不同的字节序,intel x86、x86-64架构处理器采用小端序,而许多网络协议采用的是大端序,因此在发送数据前需要将小端序转换成大端序进行发送,接收后也要转换成本地字节序进行存储。在Socket编程中主要通过以下接口来实现字节序的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//IP转换函数:
//struct in_addr是一个存储IP地址的结构体,用于将结构体的IP地址转换成点分十进制字符串,记得需要char*强类型转换
char *inet_ntoa(struct in_addr in);
/*
struct in_addr {
__be32 s_addr; //__be32=unsigned int
};
*/

//将点分十进制IP地址字符串转换成网络字节序(大端序),如“192.168.xx.xx”转换成网络字节序
in_addr_t inet_addr(const char *cp); //in_addr_t=unsigned int

//端口转换函数:
#include <arpa/inet.h>
//本地字节序到网络字节序,例如服务器的bind、客户端的connect
uint32_t htonl(uint32_t hostlong);//long,4字节数
uint16_t htons(uint16_t hostshort);//short,2字节

//网络字节序到本地字节序,例如接收客户端的协议消息,accept
uint32_t ntohl(uint32_t netlong); //long,4字节数
uint16_t ntohs(uint16_t netshort); //short,2字节

基于TCP的Socket编程

整体流程框架如下:TCP的Socket编程

socket编程相关API介绍

客户端:
1. socket():创建套接字文件,既用于连接也用于通信; 2. connect():发起连接请求;
3. send():发送数据;
4. recv():接收数据;
5. close():关闭文件描述符;

服务器端:
1. socket():创建套接字文件,用于连接;
2. bind():把socket返回的文件描述符和IP、端口号进行绑定;
3. listen():监听,将socket返回的文件描述符属性由主动变为被动;
4. accept():阻塞函数,阻塞等待客户端连接请求,如果有客户端连接,则accept函数返回一个用于通信的套接字文件;
5. recv():接收客户端发送的数据,read()也可;
6. send():发送数据;
7. close():关闭文件连接、通信两个描述符;

1. 创建套接字socket()
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
/*************************************************
头文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

函数:
int socket(int domain, int type, int protocol);

参数:
domain:协议族,常用:
AF_UNIX, AF_LOCAL 本地通信
AF_INET IPv4 Internet protocols
AF_INET6 IPv6 Internet protocols

type:套接字类型,常用:
SOCK_STREAM 流式套接字,TCP
SOCK_DGRAM 数据报套接字,UDP

protocol:使用的具体协议,需要和type和domain匹配,填0由系统自动匹配;
传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
网络层:htons(ETH_P_IP(仅IP协议)、ETH_P_ARR(仅ARR协议)、ETH_P_ALL(所有类型的以太网协议))

返回值:
成功:返回文件描述符
失败:-1
*************************************************/
2. 绑定套接字bind()

将套接字与IP和端口号绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*************************************************
头文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

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

参数:
sockfd:
addr:struct sockaddr是一个通用结构体,通用的意思是每种不同的协议(Ipv4、Ipv6等)都有不同的结构体,如果套接字绑定时根据不同的结构体进行绑定就会需要更多的函数接口,为了避免这种情况,任何结构体传入时都需要通过强制类型转换至struct sockaddr;结构体细节:
struct sockaddr {
sa_family_t sa_family; //2字节,typedef unsigned short int sa_family_t;
char sa_data[14]; //14字节
}

addrlen:socklen_t实际上就是unsigned int,表示第二个参数结构体的大小;

返回值:
成功:0
失败:-1
*************************************************/
通过以下例子来体会一下ipv4协议结构体类型转换到通用结构体的细节,假设现在有ipv4结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct sockaddr_in {
__kernel_sa_family_t sin_family; //ipv4协议,2字节typedef unsigned short __kernel_sa_family_t;
__be16 sin_port; // 端口号,2字节typedef unsigned short __be16
struct in_addr sin_addr;// IP地址,4字节
/*实际上结构体为:
struct in_addr {
__be32 s_addr; //unsigned int,IP地址,4字节
};
*/

//这里的unsigned char数组存在的意义就是将结构体大小撑到通用结构体的16字节
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)]; //__SOCK_SIZE__ =16,这部分是8字节
};
ipv4协议的结构体在头文件<netinet/in.h>中定义,因此传值时需要通过该sockaddr_in进行传值,再通过强制类型转换至结构体,详细参考实现部分;这里需要传入IP地址和端口号,IP地址可以写入本地的IP地址,注意如果使用Windows的工具和Linux虚拟机进行通信,需要保证是桥接模式,也即两个系统必须处于同一个IP网段下,ubuntu查看ip可以通过:
1
ip addr show
如果安装了网络工具,也可以:
1
ifconfig
而windows下就是:
1
ipconfig

3. 监听套接字listen()

监听,将主动套接字变成被动套接字,使得套接字处于一个监听状态等待客户端连接请求;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*************************************************
头文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

函数:
int listen(int sockfd, int backlog);

参数:
sockfd:套接字标识符
backlog:同时连接服务器的客户端最大个数,已连接队列(完成三次握手,等待accept)、等待连接队列(握手开始,但未完成)总共最大能够容纳的连接数量。

返回值:
成功:0
失败:-1
*************************************************/

4. 阻塞等待连接accept()

阻塞函数,阻塞等待客户端的连接请求,如果有客户端申请连接会返回一个用于通信的套接字;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*************************************************
头文件:
#include <sys/socket.h>

函数:
int accept(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);

参数:
socket:套接字ID
address:同bind,用于accept时存储客户端的协议、IP、端口结构体,如果无需获知客户端结构体信息,传入NULL;
address_len:address的结构体大小,不关心可以为NULL,一旦address被使用,len不能设置为NULL(会导致Bad address)。
restrict是一个C语言(C99)引入的关键字,用于告诉编译器程序其他部分不会通过其他指针对其进行修改或访问,编译器因此能够将其优化成较为高效的机器码;

返回值:
成功:返回新的套接字标识ID
失败:-1
*************************************************/

5. 接收数据recv()
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
/*************************************************
头文件:
#include <sys/types.h>
#include <sys/socket.h>

函数:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数:
sockfd:套接字标识ID
buf:接收缓冲区,例如一个char数组
len:缓冲区大小,如sizeof(char数组)
flags:接收数据的特殊行为,一般为0代表没有特殊行为,其他例子如下:
MSG_OOB:接收带外数据(带外数据是被 发送端标记为紧急的数据)
MSG_PEEK:预览来自缓冲区的数据,而不会将其移除;
MSG_WAITALL:阻塞等待所有数据接收完才返回;
MSG_DONTWAIT:没有读取到数据立马返回,尽管套接字被设置成阻塞的;
MSG_TRUNC:接收数据大于缓冲区长度,截断并且返回实际的数据长度;


返回值:
失败:-1
客户端关闭且没有接收到信息:0
成功接收到的数据大小:其他正整数
*************************************************/
6. 请求连接connect()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*************************************************
头文件:
#include <sys/socket.h>

函数:
int connect(int socket, const struct sockaddr *address,
socklen_t address_len);

参数:
socket:套接字文件描述符
address:协议、IP、端口地址结构体,表明客户端申请连接哪个服务器IP和端口
address_len:结构体大小

返回值:
成功:0
失败:-1
*************************************************/
7. 发送数据send()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*************************************************
头文件:
#include <sys/socket.h>

函数:
ssize_t send(int socket, const void *buffer, size_t length, int flags);

参数:
socket:套接字文件描述符
buffer:发送缓冲区,例如char数组
length:缓存区大小
flags:发送的特殊行为

返回值:
失败:-1
没有数据被发送,或者发送异常(TCP协议):0
成功发送的字节数:其他正整数
*************************************************/

而对于close就不是socket变成特有的,socket返回的本质也是一种文件描述符,所以socket的close函数和操作普通文件、IPC通信方式没有什么不同。

客户端-服务器的socket编程实现

根据上面的函数,这里给出了一种简单实现通信的方法,服务器、客户端通过ip:端口实现字符串的发送和接收:

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
//server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> // for sockaddr_in,也可<linux/in.h>,但会与inet.h冲突
#include <arpa/inet.h> //for htons,iner_addr
#include <string.h>
#include <unistd.h>

#define MAXSIZE 128

int main(){
int sockfd;
//创建socket
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0){
perror("socket failed");
return -1;
}
//绑定socket
struct sockaddr_in serveraddr;
serveraddr.sin_family=AF_INET;
serveraddr.sin_port=htons(8888);
serveraddr.sin_addr.s_addr=inet_addr("192.168.218.135");
if(bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))<0){
perror("bind failed");
return -1;
}
printf("bind OK\n");
//监听socket
if(listen(sockfd,8)<0){
perror("listen failed");
return -1;
}
printf("listen OK\n");
//等待socket请求
int acceptfd;
acceptfd=accept(sockfd,NULL,NULL);
if(acceptfd<0){
perror("accept failed");
return -1;
}
//通过socket获取信息
char buf[MAXSIZE];
while(1){
recv(acceptfd,buf,sizeof(buf),0);
printf("buf=%s\n",buf);
if(strncmp(buf,"quit",4)==0)
break;
}
printf("OK\n");
//关闭socket
close(acceptfd);
close(sockfd);
}

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
//client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> // for sockaddr_in,也可<linux/in.h>,但会与inet.h冲突
#include <arpa/inet.h> //for htons,iner_addr
#include <unistd.h> //close
#include <string.h>

#define MAXSIZE 128

int main(){
//创建socket
int sockfd=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serveraddr;
serveraddr.sin_family=AF_INET;
serveraddr.sin_port=htons(8888);
serveraddr.sin_addr.s_addr=inet_addr("192.168.218.135");
//发起连接请求
if(connect(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0){
perror("connect failed");
return -1;
}
//发送请求
char buf[MAXSIZE];
while(1){
fgets(buf,32,stdin);
if(strncmp(buf,"quit",4)==0)
break;
send(sockfd,buf,sizeof(buf),0);
}
//关闭socket
close(sockfd);
return 0;
}

上述程序存在的问题

上述程序虽然可以实现正常的信息收发,但仍存在一些可以优化方面:

  1. 发送数据时最后输入的是回车,也即,该字符会被当成普通字符发送,导致服务器接收数据也空出一行;

    解决:人为将buf最后一个字符()修改成结束符\0;

  2. 客户端进程异常结束、或者直接结束,会导致服务器接收数据无限刷屏;

    解决:客户端进程结束,recv返回0,上面的代码没有针对这个行为进行处理和定义,所以会出现死循环局面;希望实现一个功能,使第一个客户端断开连接后,服务器能够重新进入accept阻塞,等待新的连接请求;

  3. 服务器Ip、端口号是写死的,在切换局域网时这个IP段很可能失效,频繁修改和重新编译;

    解决:通过命令行传入服务器IP、端口信息;

  4. 目前的服务器-客户端是典型的单工通信,客户端发,而服务器收并打印;

    解决:首先简单的是服务器收到消息,能够向客户端响应ok;但这还不是真正意义的双工通信,因此必须借助多进程的方式,父进程复制发,子进程负责接收;

  5. 目前的服务器只能响应一个客户端的连接请求,无法同时响应多个客户端;

    解决:引入并发编程的知识,在本节未引入这种并发处理机制,在后面引入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
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> // for sockaddr_in,也可<linux/in.h>,但会与inet.h冲突
#include <arpa/inet.h> //for htons,iner_addr
#include <string.h>
#include <unistd.h> //close
#include <stdlib.h> //atoi
#include <sys/wait.h>

#define MAXSIZE 128

int main(int argc,char*argv[]){
if(argc!=3){
printf("Please input ./server <ip> <port>\n");
return -1;
}
//创建socket
int sockfd;
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0){
perror("socket failed");
return -1;
}
//绑定socket,包括服务器端协议、IP、端口
struct sockaddr_in serveraddr;
serveraddr.sin_family=AF_INET;
serveraddr.sin_port=htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr=inet_addr(argv[1]); //服务器端也可以使用宏:htonl(INADDR_ANY),此时命令参数任意输入数字即可
if(bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))<0){
perror("bind failed");
return -1;
}
printf("bind OK\n");
//转变为监听状态
if(listen(sockfd,8)<0){
perror("listen failed");
return -1;
}
printf("listen OK\n");
//处理请求
int acceptfd;
char sendbuf[MAXSIZE]={0};
char buf[MAXSIZE]={0};
pid_t pid;
int recvbyte;
while(1){
printf("Wait Connect\n");
//accept放在循环中,如果进程结束会重新接收客户端请求
acceptfd=accept(sockfd,NULL,NULL);
if(acceptfd<0){
perror("accept failed");
return -1;
}
printf("Client Connect!\n");
//多进程模式实现双工通信
pid=fork();
if(pid<0){
perror("fork failed");
return -1;
}
else if(pid==0){
while(1){
//进程循环接收客户端数据
recvbyte=recv(acceptfd,buf,sizeof(buf),0);
if(recvbyte<0){
perror("receive failed");
break;
}
else if(recvbyte==0){ //代表客户端关闭(前提是阻塞接收数据)
break; //跳出循环,重新接收accept
}
else{ //接收正常,打印
printf("Received=%s\n",buf);
if(strncmp(buf,"quit",4)==0){
break;
}
}
}
//exit(0); //跳出循环不要立刻退出进程,否则无法跳出循环接收新的请求
}
else{
while(1){
//父进程发送数据
fgets(sendbuf,32,stdin);
sendbuf[strlen(sendbuf)-1]='\0';
send(acceptfd,sendbuf,sizeof(sendbuf),0);
//接收到quit,服务器应该重新接收新accept,注意不是杀死进程,服务器进程不应该轻易杀死
if(strncmp(buf,"quit",4)==0){
break;
}
}
//wait(NULL);
close(acceptfd);
close(sockfd);
printf("OK\n");
exit(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
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> // for sockaddr_in,也可<linux/in.h>,但会与inet.h冲突
#include <arpa/inet.h> //for htons,iner_addr
#include <unistd.h> //close
#include <string.h>
#include <stdlib.h> //atoi
#include <sys/wait.h>

#define MAXSIZE 128

int main(int argc,const char*argv[]){
if(argc!=3){
printf("Please input ./client <ip> <port>\n");
return -1;
}
//创建socket
int sockfd=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serveraddr;
serveraddr.sin_family=AF_INET;
serveraddr.sin_port=htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr=inet_addr(argv[1]);
//请求连接
if(connect(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0){
perror("connect failed");
return -1;
}
//多进程实现双工通信
pid_t pid=fork();
char buf[MAXSIZE]={0};
char recvbuf[MAXSIZE]={0};
if(pid<0){
perror("fork failed");
return -1;
}
//子进程发送数据
else if(pid==0){
while(1){
fgets(buf,32,stdin);
buf[strlen(buf)-1]='\0';
send(sockfd,buf,sizeof(buf),0);
if(strncmp(buf,"quit",4)==0){
kill(getppid(),SIGKILL); //发送quit的话直接杀死整个客户端程序,包括父进程
break;
}
}
exit(0);
}
else{
//父进程接收并且打印数据
while(1){
recv(sockfd,recvbuf,sizeof(recvbuf),0);
printf("Server send:%s\n",recvbuf);
}
wait(NULL);
close(sockfd);
exit(0);
}
}

基于UDP的Socket编程

UDP Socket通信流程 UDP是面向无连接的通信协议,因此结构十分简单;除了无需建立连接,UDP使用发送数据包、接收数据包的函数和TCP也不完全一样;

UDP相关API介绍

socket、bind与TCP基本一致,注意其中socket的套接字类型需要使用数据报类型;

  1. 接收数据包recvfrom() 因为UDP是无连接的,因此在发送、接收的函数参数都额外带有结构体消息,代表该数据包遵循什么IP协议、来自哪个IP和端口,其余参数与TCP的类似。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /*************************************************
    头文件:
    #include <sys/types.h>
    #include <sys/socket.h>

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

    参数:
    sockfd:套接字文件描述符
    buf:接收缓冲区,如一个char数组
    len:缓冲区大小
    flags:接收的特殊行为,无就是0
    src_addr:同TCP的accept类似,标识客户端的协议、IP、端口结构体,接收了可以打印或者其他用途
    addrlen:结构体大小,注意这里是指针,因此sizeof后还要取其地址。

    返回值:
    失败:-1
    客户端发送窗口关闭,或者没接收到数据:0
    正常接收到数据的字节数:其他正数
    *************************************************/

  2. 发送数据包sendto()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /*************************************************
    头文件:
    #include <sys/types.h>
    #include <sys/socket.h>

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

    参数:
    除addrlen外同recv基本类似,不再赘述;注意的是这里的结构体是发送方的结构体,且addrlen不是指针类型,直接sizeof计算即可,无需取地址。

    返回值:
    同recvfrom一致;
    *************************************************/

sendto、recvfrom指定了结构体的信息,对应UDP无连接的特点,但是这个并不是硬性的写法。在UDP中,也可以使用TCP的send、recv函数,但是前提是UDP使用了connect函数进行绑定,此时的connect函数只是起绑定服务器IP、端口、协议作用,并不是申请连接;同样的,TCP也可以使用sendto、recvfrom,用于TCP已经存在连接关系,所以对应函数结构体传NULL即可,一样能完成信息的传输。

简单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
38
39
40
41
42
43
44
45
//server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>

#define MAXSIZE 128

int main(int argc,const char *argv[]){
//判断命令行参数是否合法
if(argc!=3){
printf("Please input ./server <ip> <port>\n");
return -1;
}
//创建socket
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0){
perror("socket failed");
return -1;
}
//绑定socket
struct sockaddr_in serveraddr,clientaddr; //IPV4结构体
serveraddr.sin_family=AF_INET; //IPV4协议
serveraddr.sin_port=atoi(argv[2]);
serveraddr.sin_addr.s_addr=inet_addr(argv[1]);
if(bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0){
perror("bind failed");
return -1;
}
printf("Client Connected!\n");
char buf[MAXSIZE]; //接收缓冲区
socklen_t client_len=sizeof(clientaddr); //计算结构体大小
while(1){
//接收消息
recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&clientaddr, \
&client_len);
printf("Received:%s\n",buf);
}
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
//client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define MAXSIZE 128

int main(int argc,const char*argv[]){
//判断命令行参数合法性
if(argc!=3){
printf("Please input ./server <ip> <port>\n");
return -1;
}
//创建socket
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0){
perror("socket failed");
return -1;
}
char buf[MAXSIZE];
//客户端声明目标服务器的IP、端口
struct sockaddr_in clientaddr;
clientaddr.sin_family=AF_INET;
clientaddr.sin_addr.s_addr=inet_addr(argv[1]);
clientaddr.sin_port=atoi(argv[2]);
while(1){
fgets(buf,32,stdin);
//发信息
sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&clientaddr,sizeof(clientaddr));
if(strncmp(buf,"quit",4)==0){
break;
}
}
close(sockfd);
return 0;
}

与TCP通信很大一个区别是,假如客户端多开,或者先打开客户端、再打开服务器端,UDP的客户端都能够给服务器发信息,这种特点使得使用UDP制作聊天室、多端通信变得简单,但另一方面,对服务器、客户端而言,这种非连接的通信方式是不安全的。

以上就是TCP、UDP的socket编程核心内容,在引入TCP并发之前,使用已有的知识引入两个项目应用,分别是基于UDP的聊天室,以及基于TCP的文件传输服务(FTP)。

项目实战:基于UDP聊天室的实现

项目描述:实现一个简单的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
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
//server.c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

//简化perror、退出逻辑
#define errolog(error) do{perror(error);exit(-1);}while(0)

#define MAXSIZE 128 //最大信息容量
#define NAME_SIZE 10 //名字容量

/*信息结构体:
信息类型:登录信息、聊天信息、退出信息
text:信息内容
name:名字
*/
typedef struct{
char type;
char text[MAXSIZE];
char name[NAME_SIZE];
}Msg;

//链表结构体
typedef struct node{
struct node* next; //next指针
struct sockaddr_in clientaddr;//存储客户端ip;
}listnode,*linklist;

//初始化链表头结点
linklist create_linklist(){
linklist H;
H=(linklist)malloc(sizeof(listnode));
if(H==NULL){
errolog("linklist create failed");
}
H->next=NULL;
memset(&(H->clientaddr),0,sizeof(H->clientaddr));
return H;
}

//登录函数————创建客户端结点,向所有客户端发送消息
void login(int sockfd,linklist H,struct sockaddr_in clientaddr,Msg mssag){
memset(&mssag.text,0,sizeof(mssag.text)); //去除垃圾内存字符
sprintf(mssag.text,"%s logined",mssag.name); //登录信息
printf("%s\n",mssag.text);
//向所有客户端发送登录信息
linklist p=H->next;
while(p!=NULL){
sendto(sockfd,&mssag,sizeof(mssag),0,(struct sockaddr*)&(p->clientaddr),sizeof(p->clientaddr));
p=p->next;
}
//新客户端登录,头插法加入链表结构管理
linklist temp=(linklist)malloc(sizeof(listnode));
temp->clientaddr=clientaddr;
temp->next=H->next;
H->next=temp;
}

//广播消息函数————向除自己外的所有用户转发消息
void transfer(int sockfd,linklist H,struct sockaddr_in clientaddr,Msg mssag){
//广播消息包括消息+Name+said等字符
char buf[MAXSIZE+NAME_SIZE+10]={0};
sprintf(buf,"%s said: %s",mssag.name,mssag.text);
printf("%s\n",buf);
strcpy(mssag.text,buf); //浅拷贝

linklist p=H->next;
while(p!=NULL){
//memcmp比较数组、结构体内容是否相等
if(memcmp(&clientaddr,&p->clientaddr,sizeof(clientaddr))==0)
p=p->next; //相等,代表当前client就是遍历到的client,无需转发给自己
else{
//其他就转发
sendto(sockfd,&mssag,sizeof(mssag),0,(struct sockaddr*)&(p->clientaddr),sizeof(p->clientaddr));
p=p->next;
}
}
}

//客户端退出函数————销毁对应客户端结点空间、转发下线消息
void quit(int sockfd,linklist H,struct sockaddr_in clientaddr,Msg mssag){
linklist p=H;
sprintf(mssag.text,"%s offline",mssag.name);
//销毁结点
while(p->next!=NULL){
if(memcmp(&p->next->clientaddr,&clientaddr,sizeof(clientaddr))==0){
linklist temp=p->next;
p->next=temp->next;
free(temp);
temp=NULL;
printf("%s\n",mssag.text);
}
//发送离线消息
else{
sendto(sockfd,&mssag,sizeof(mssag),0, \
(struct sockaddr*)&p->next->clientaddr,sizeof(p->next->clientaddr));
p=p->next;
}
}

}

int main(int argc,char*argv[]){
if(argc!=3){
errolog("Please input ./server <ip> <port>");
}
int sockfd;
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0){
errolog("socket failed");
}
struct sockaddr_in serveraddr,clientaddr;
serveraddr.sin_family=AF_INET;
serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
serveraddr.sin_port=htons(atoi(argv[2]));

if(bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0){
errolog("bind failed");
}
pid_t pid=fork();

Msg mssag;
memset(&mssag,0,sizeof(mssag)); //填充信息结构体,防止垃圾字符
socklen_t clientaddr_len=sizeof(clientaddr);

if(pid<0){
errolog("fork failed");
}
else if(pid==0){ //服务器子进程发送消息
while(1){
//填充消息结构体
strcpy(mssag.name,"server");
fgets(mssag.text,MAXSIZE,stdin);
mssag.text[strlen(mssag.text)-1]='\0';
strcat(mssag.text,"(from server)");
mssag.type='B';
sendto(sockfd,&mssag,sizeof(mssag),0, \
(struct sockaddr*)&(serveraddr),sizeof(serveraddr));
}
}
else{ //父进程接收消息
linklist H=create_linklist();
while(1){
recvfrom(sockfd,&mssag,sizeof(mssag),0,(struct sockaddr*)&clientaddr,&clientaddr_len);
//printf("Received:%s\n",mssag.text);
//根据消息类型决定服务器策略
switch(mssag.type){
case 'L'://登录信息
login(sockfd,H,clientaddr,mssag);
break;
case 'B': //聊天信息
transfer(sockfd,H,clientaddr,mssag);
break;
case 'Q': //退出信息
quit(sockfd,H,clientaddr,mssag);
break;
}

}
}
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
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>


#define MAXSIZE 128
#define NAME_SIZE 10
#define errolog(error) do{perror(error);exit(-1);}while(0)

//消息结构体,与服务器一致
typedef struct{
char type; //login(L),broadcast(B),quit(Q)
char text[MAXSIZE];
char name[NAME_SIZE];
}Msg;


int main(int argc,char*argv[]){
if(argc!=3){
errolog("Please input ./server <ip> <port>\n");
}
int sockfd;
sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0){
errolog("socket failed");
}
struct sockaddr_in clientaddr,serveraddr;
socklen_t serveraddr_len = sizeof(serveraddr);
clientaddr.sin_family=AF_INET;
clientaddr.sin_addr.s_addr=inet_addr(argv[1]);
clientaddr.sin_port=htons(atoi(argv[2]));
Msg mssag;
memset(&mssag,0,sizeof(mssag));//初始化内存,防止未定义字符
printf("Input name>>");
fgets(mssag.name,NAME_SIZE,stdin);
mssag.name[strlen(mssag.name)-1]='\0';
mssag.type='L';
sendto(sockfd,&mssag,sizeof(mssag),0, \
(struct sockaddr*)&clientaddr,sizeof(clientaddr));
printf("login success!\n");
pid_t pid=fork();
if(pid<0){
errolog("fork failed");
}
if(pid==0){ //子进程发送消息
while(1){
printf("Input Message>>");
fgets(mssag.text,MAXSIZE,stdin);
mssag.text[strlen(mssag.text)-1]='\0';
if(strncmp(mssag.text,"quit",4)==0){
mssag.type='Q';
sendto(sockfd,&mssag,sizeof(mssag),0, \
(struct sockaddr*)&clientaddr,sizeof(clientaddr));
printf("See you Next Time\n");
kill(getppid(),SIGKILL); //客户端退出杀死进程
exit(0);
} //填充信息结构体
mssag.type='B';
sendto(sockfd,&mssag,sizeof(mssag),0, \
(struct sockaddr*)&clientaddr,sizeof(clientaddr));

}
}
else{
while(1){
recvfrom(sockfd,&mssag,sizeof(mssag),0, \
(struct sockaddr*)&serveraddr,&serveraddr_len);
printf("%s\n",mssag.text);
}
}
close(sockfd);

return 0;
}

编译后运行方法:

1
2
./server +任意数字+端口号
./client +IP+端口号,本地IP获取通过ip addr show

该聊天室可以使不同的主机设备等在同一局域网内进行通信,如果需要实现真正的远程通信,需要内网穿透实现外网对内网的访问,那么有三种方法实现端口的映射,分别是: 1. 花生壳等网络工具,实现内网穿透;
2. 路由器端口映射(需要路由器的管理员权限),将内网端口映射出去;
3. 服务器转发;

其余内容有空再研究。

项目实战:基于TCP的文件传输服务

项目描述:实现一个客户端-服务器系统,实现客户端对服务器文件列表的请求、客户端向服务器push文件,客户端从服务器clone文件。

IO多路复用

IO模型

IO模型是计算机用于描述数据在内核空间和用户空间之间传输方式的一种概念,它关注程序在请求输入、输出操作(文件操作、socket传输)时,数据如何传输,程序的控制权如何转移。分为以下几种IO模型:

  1. 阻塞IO(Blocking IO,BIO)
    应用程序发起一个IO操作,然后进入阻塞等待,直到数据准备好并被拷贝到用户空间;例如read函数的读事件,用户调用read实际上触发了内核系统调用read函数,从特定位置读取数据到内核缓冲区,再复制到用户缓冲区,用户缓冲区读取到数据read函数才会返回,用户进程才能运行其他代码或者对下一个文件描述符发起IO操作。

特点最常用、最简单,但效率最低

  1. 非阻塞IO(Non-Blocking IO,NIO)
    这是通过定期轮询实现的;如果数据未准备好,调用立刻返回执行其他操作,定期检查IO操作是否可以进行;例如read函数也可以通过标志位参数设置非阻塞模式,每个一段时间会查看其是否返回数据,否则就继续执行其他任务。

特点:防止进程阻塞,但是需要定期轮询(实际上这也十分浪费CPU资源);

  1. IO多路复用(IO Multiplexing)
    IO多路复用允许应用程序通过某些机制(select、poll、epoll)同时监控多个IO请求,应用程序仍然会阻塞等待但是只需要等待多个IO操作任何一个完成,提高了效率。

特点:允许同时对多个IO进行监测,是并发编程的重要操作。

  1. 信号驱动IO(Signal-Driven IO) 应用程序首先告知操作系统如果数据准备好了,请发送信号通知它,然后应用程序会继续执行其他任务;当数据就绪,操作系统会向应用程序发送信号,然后应用程序进行IO操作。

特点:在系统驱动中比较常用。

非阻塞模式实现

打开一个文件、创建一个socket返回的文件描述符属性默认均是阻塞的,如果需要设置成非阻塞模式,可以通过函数接口fcntl实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*************************************************
头文件:
#include <unistd.h>
#include <fcntl.h>

函数:
int fcntl(int fd, int cmd, ... /* arg */ );

参数:
fd:文件描述符
cmd:命令参数
arg:命令对应的其他需要的参数

返回值:
成功:0或者其他标识符
失败:-1
*************************************************/

fcntl的命令参数、返回值比较多,对于将文件描述符设置成非阻塞模式,只需要理解这种写法:

1
2
3
int flag=fcntl(sockfd,F_GETFL);//F_GETFL获取现在文件描述符(如socket)的属性
flag|=O_NONBLOCK; //int是32为,对应32个属性,或操作相对于把这个属性对应的数字标1了
fcntl(sockfd,F_SETFL,flag);//F_SETFL通过新的flag设置为新属性

示例:将输入流设置成非阻塞,程序无需一直等待用户输入:

1
2
3
4
5
//stdin对应的文件描述符是0;(输出是1,错误是2)
flag=fcntl(0,F_GETFL); //非阻塞设置
flag|=O_NONBLOCK;
fcntl(0,F_SETFL,flag);
fgets(buf,32,stdin); //非阻塞接收

同步IO与异步IO

IO操作内核完成的任务有两个阶段第一通过系统调用从特定位置读取数据到内核缓冲区(数据就绪),或者对于写事件来说就是内核缓冲区有一定空间内核能够写入。第二阶段就是内核将数据从内核缓冲区写入用户缓冲区(copyout),或者从用户缓冲区写入内核缓冲区(copyin)

异步IO(Asynchronous I/O)两个阶段都不是阻塞的,意味着用户进程只需要发起IO操作,然后就能立刻执行其他任务,内核会自动监听对应的读写事件,并且完成内核和用户态数据的拷贝,任务完成,会通过回调函数、信号等方式通知用户,用户进程只需要在用户缓冲区使用数据即可

同步IO(Synchronous I/O):同步IO的第二阶段是阻塞的,也即内核通知用户进程,用户进程需要阻塞地等待内核将数据从内核缓冲区拷贝到用户缓冲区,或者反向。

IO多路复用中,select/poll/epoll都是同步IO模型,因为它们只是优化了第一阶段,即监听数据是否就绪来通知用户进程,用户进程需要进一步使用read/write等让内核完成数据跨态传输

水平触发(LT)和边沿触发(ET)

在Linux中,水平触发(Level-Triggered)边缘触发(Edge-Triggered)是针对IO多路复用的术语。

select/poll/epoll能监听哪些文件描述符发生了读事件、写事件,读事件指内核数据就绪,写事件指内核空间就绪,用户进程可以进一步读写数据。

水平触发:只要读事件发生(有数据到达、TCP socket的accept中有连接请求到达/FIN请求(通知数据不再发送)、管道/FIFO写端关闭(通知数据不再发送)),或者写事件发生(内核缓冲区空闲一定的空间、TCP socket的connect建立),内核就会向用户进程通报只要数据没读走、或者仍然存在可写入的空间,内核就会持续通知,表现在用户始终能通过select、poll或者epoll_wait返回的参数获取事件,并且继续完成数据读写。

边缘触发:三种IO多路复用只有epoll支持定义边缘触发边缘触发的触发条件是缓冲区状态变化,例如读事件时,内核数据从无到有,不可读变为可读;写事件时,内核缓冲区从不可写变成可写,才会触发,而且仅仅触发一次,知道新状态发生。如果本次读取的数据没有处理完成,那么再使用epoll_wait也无法获取到事件了,只能等到下次状态更新,才能获取事件。

区别:

  1. 效率上,边缘触发的效率更高,因为它只触发一次,不会重复触发,意味着它的系统调用更少,因为它无需反复返回可读写的事件。

  2. 边缘触发需要谨慎处理数据,防止数据丢失,水平触发的数据处理则安全很多。

一般读事件适用水平触发,能够持续获取缓冲区的数据,按照业务逻辑界定和读取。使用边缘触发的epoll一般用于写事件,因为当没有数据要写入时,重复的触发没有太大意义。

IO多路复用

IO多路复用用于同时监测多个IO数据是否就绪,这样就无需反复通过系统调用去轮询、顺序处理IO。

IO多路复用:select

select基本原理:select定义了三种事件的集合,分别是读事件写事件、异常事件,它们用三个位图(bitmap)结构表示,例如关注输入流的读事件,那么读事件位图某个文件描述符编号就被置1。用户批量地将位图设置好,该位图就会拷贝进去内核内核通过遍历轮询返回数据就绪的位图拷贝回用户空间,用户遍历位图对这些产生事件文件描述符操作,可见对n个文件描述符进行事件监测,每次拷贝的复杂度是O(n)

用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
24
25
26
27
28
29
30
/*************************************************
头文件:
#include <sys/select.h>
/* According to earlier standards */
#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);

辅助函数,配合select一起使用:
void FD_CLR(int fd, fd_set *set); //清空集合中文件描述符为fd的字符
int FD_ISSET(int fd, fd_set *set); //判断描述符fd是否在事件集合中
void FD_SET(int fd, fd_set *set); //将文件描述符fd加入事件集合中
void FD_ZERO(fd_set *set); //清空集合中所有文件符

参数:
nfds:最大的文件描述符个数
readfds:读事件集合(最常用的监测事件),指向可进行读操作的文件描述符(例如socket有数据到达)
writefds:写事件集合,指向可进行非阻塞写操作的文件描述符(例如socket可以直接非阻塞发送数据)
exceptfds:异常事件集合,例如TCP的带外数据(紧急数据)
以上三个参数对某个参数不关心可传NULL;
timeout:超时时间,代表IO事件发生的最长时间,传NULL代表无限等待

返回值:
失败:-1
有事件发生:>0
*************************************************/

值得注意的是,select函数调用返回后,会清除集合内其他事件的监听;例如使用select函数同时检测键盘(stdin)和鼠标(字符驱动设备文件),将其同时加入读事件集合中,假设鼠标移动(鼠标产生读事件),select函数会返回并且将未产生读事件的文件描述符(键盘)清除,然后尽管再输入,select函数也无法监听键盘输入事件了;

select监听事件:键入or鼠标

因此如果需要同时对两个设备进行监听,只能再每次循环开始前重新将完整集合赋给监听集合(tempfd=readfds):

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

#define errlog(error) do{perror(error);exit(-1);}while(0)

int main(int argc,const char*argv[]){
fd_set readfds,tempfd; //读事件集合
FD_SET(0,&readfds); //监听键盘输入
int mouse0_fd=open("/dev/input/mouse0",O_RDONLY); //Linux设备文件
printf("%d",mouse0_fd);
FD_SET(mouse0_fd,&readfds);//监听鼠标
int slid;
while(1){
tempfd=readfds;
slid=select(mouse0_fd+1,&tempfd,NULL,NULL,NULL);
if(slid<0){
errlog("select failed");
}
if(FD_ISSET(0,&tempfd)){
printf("Keying\n");
}
else if(FD_ISSET(mouse0_fd,&tempfd)){
printf("Mousing\n");
}
}
close(mouse0_fd);
return 0;
}
确认鼠标文件位置:鼠标事件在Linux下也是一种文件,一般存储与/dev/input/mousex,对于多个文件,通过命令
1
sudo cat /dev/input/mousex
执行时也必须使用管理者权限:
1
sudo ./listen
如果当前文件为鼠标文件,当鼠标移动时会显示增加内容。

select实现简单TCP并发通信

select的监听机制使得可以同时监听多个端口,只要有客户端连接,select会返回监听事件,只需要对监听事件进行判断,如果是socket引起的就accept,实现并发通信,一个简单的单工通信示例如下,服务器能够同时接收多个客户端的信息,并且打印其IP和端口号,因为只把socket列为读事件,因此该程序是单工通信的,如果增加输入的监听,则也能轻易在接收消息时发送相应,但要实现完全的双工通信,可能需要考虑监听写事件。

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
//服务器端server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define errlog(error) do{perror(error);exit(-1);}while(0)
#define MAXSIZE 128

int main(int argc,const char*argv[]){
if(argc!=3){
printf("Please Input ./server <ip> <port>\n");
return -1;
}
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0){
errlog("socket failed!\n");
}
struct sockaddr_in serveraddr,clientaddr;
serveraddr.sin_family=AF_INET;
serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
serveraddr.sin_port=htons(atoi(argv[2]));
if(bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0){
errlog("bind failed");
}
if(listen(sockfd,8)<0){
errlog("listen failed");
}
printf("listen Done\n");
socklen_t clien_len=sizeof(clientaddr);
fd_set readfds,tempfds;
//初始化清空集合内所有文件符
FD_ZERO(&readfds);
FD_ZERO(&tempfds);
FD_SET(sockfd,&readfds);//监听socket
int maxfd=sockfd+1;
char recvbuf[MAXSIZE];
int acceptfd,recvbytes;
while(1){
tempfds=readfds;
int slid=select(maxfd,&tempfds,NULL,NULL,NULL);
if(slid<0){
errlog("select failed");
}
else{
//如果监听多个文件,需要使用for结构遍历进行判断(在此例不是必要的,因为只监听了socket)
for(int i=0;i<maxfd;i++){
if(FD_ISSET(i,&tempfds)){
if(i==sockfd){ //如果socket发生读事件(有新申请到达),接受
acceptfd=accept(sockfd,(struct sockaddr*)&clientaddr,&clien_len);
if(acceptfd<0){
errlog("accept failed");
}
printf("client IP:%s,Port:%d\n", \
(char*)inet_ntoa(clientaddr.sin_addr), \
ntohs(clientaddr.sin_port));
//增加监听新的接收套接字
FD_SET(acceptfd,&readfds);
//更新监听文件描述符数量
maxfd=acceptfd>maxfd?acceptfd+1:maxfd;
}
else{
recvbytes=recv(i,recvbuf,sizeof(recvbuf),0);
//客户端退出,清除其监听任务,给其他客户端使用
if(recvbytes<=0){
FD_CLR(i,&readfds);
close(i);
continue;
}
printf("Received:%s\n",recvbuf);
}
}
}
}
}
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
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/sockv et.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>


#define errlog(error) do{perror(error);exit(-1);}while(0)
#define MAXSIZE 128

int main(int argc,const char*argv[]){
if(argc!=3){
printf("Please Input ./server <ip> <port>\n");
return -1;
}
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0){
errlog("socket failed!\n");
}
struct sockaddr_in clientaddr;
clientaddr.sin_family=AF_INET;
clientaddr.sin_addr.s_addr=inet_addr(argv[1]);
clientaddr.sin_port=htons(atoi(argv[2]));
char sendbuf[MAXSIZE];
char recvbuf[MAXSIZE];
if(connect(sockfd,(struct sockaddr*)&clientaddr,sizeof(clientaddr))<0){
errlog("connect failed");
}
pid_t pid=fork();
if(pid<0){
errlog("fork failed");
}
else if(pid == 0){
while(1){
recv(sockfd,recvbuf,sizeof(recvbuf),0);
printf("Server:%s\n",recvbuf);
}

}
else{
while(1){
fgets(sendbuf,32,stdin);
send(sockfd,sendbuf,sizeof(sendbuf),0);
}
close(sockfd);
return 0;
}
}

select机制虽然能够实现并发TCP,但是也存在着弊端,表现在:
1. 单个进程的select一般最多能够监听1024/2048个文件描述符
2. select的监听实际上也是基于轮询的机制
3. 涉及到用户态和内核态的数据拷贝;具体而言,从用户态到内核态,表现在读、写、异常事件时数据需要拷贝到内核,因为监听发生在内核,在内核的监听是阻塞的,除非调用超时、或者有监听IO发生返回,才重新唤醒,唤醒后才从内核态切换回用户态。

IO多路复用:poll

poll基本原理:poll使用结构体数组管理事件集合一个文件描述符就是一个结构体,在结构体中定义这个描述符关心的事件,将结构体数组拷贝进去内核。内核同样会遍历结构体数组,将发生的事件填入结构体字段返回,返回的只是用户关心的文件描述符结构体,无需整个数组返回,因此返回拷贝的复杂度是O(1)。另一方面监听数量取决于内存大小,不再有位图那样的大小限制。

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
/*************************************************
头文件:
#include <poll.h>

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

/*struct pollfd结构体:
struct pollfd {
int fd; //需要监听的文件描述符
short events; //关心文件描述符事件,如读(POLLIN)、写(POLLOUT)、紧急(POLLPRI)等等
short revents; //如果关心的事件发生,该成员的值会被填充,填充值和事件标识一样
};
*/

参数:
fds:监听事件的结构体指针,一般写成数组的写法,例如struct pollfd fds[N];
nfds:结构体对象个数N,和N一致;
timeout:超时时间,单位为ms,-1代表无限等待(阻塞)。

返回值:
成功:一个正数
失败:-1
没有事件发生或者超时: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
#include <stdio.h>
#include <signal.h>
#include <poll.h> //for poll
#include <sys/stat.h>
#include <fcntl.h> //for open
#include <unistd.h>

/*已在头文件定义,无需写出
struct pollfd{
int fd; //监听文件描述符
short events; //监听事件类型
short revents; //发生则返回类型
};
*/

int main(){
struct pollfd pfd[2];
pfd[0].fd = 0; //输入流
pfd[0].events = POLLIN; //读事件
int mouse_fd = open("/dev/input/mouse0",O_RDONLY);
pfd[1].fd = mouse_fd; //鼠标流
pfd[1].events = POLLIN;
while(1){
poll(pfd,2,4000); //2个事件、4s阻塞
if(pfd[0].revents&POLLIN){ //输入流读事件产生
printf("Keying!\n");
}
if(pfd[1].revents&POLLIN){ //鼠标读事件产生
printf("Mouse Moving!\n");
}
}
close(mouse_fd);
return 0;
}

mouse设备文件是管理者权限,执行时需要sudo权限。

IO多路复用:epoll

epoll是Linux特有的一种实现高并发(百万)的方式,不过其他系统也有类似的实现方法。它能够解决select和poll机制下存在的几个问题,具体是:
1. 支持文件描述符没有限制。与poll类似,epoll支持的文件描述符数量取决于系统内存,适用于高并发场景,且优于poll的是,epoll性能不会随着监听文件数量增多导致性能的下降

  1. epoll没有轮询机制;它只处理活跃的文件描述符,每个文件描述符都有自带的回调函数,活跃的文件描述符会自动调用,提高了效率;

epoll基本原理:数据管理思路和poll差不多,但epoll采取了更加高效的数据结构——红黑树和双向链表。epoll_ctl会在红黑树注册一个结点,一个文件描述符就是一个结点,结点也包含关心的事件(读事件、写事件等)信息,同时会在设备的驱动层为这个文件描述符注册一个回调函数。这个回调函数会在设备中断到达(数据到达)时将结点添加到双向链表结构的就绪队列中,且内核会将就绪情况报告用户,用户调用epoll_wait从就绪队列取出结点并且进行读写。红黑树的增删复杂度都是稳定的O(logn),就绪链表插入和删除复杂度是O(1),因此当大量文件描述符需要IO操作时效率很高