socket网络编程

socket网络编程

通用socket地址

socket地址其实是一个结构体,封装了端口号和IP等信息

socket网络编程中表示socket地址的是结构体sockaddr:

1
2
3
4
5
6
7
//不能使用ipv6
#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family;//地址族类型
char sa_data[14];//存放socket地址值
};
typedef unsigned short int sa_family_t;
1
2
3
4
5
6
7
8
9
//可以使用ipv6
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;

专用socket地址 (IPV4 & IPV6)

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
#include <netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in6
{
sa_family_t sin6_family;
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。

IP地址转换(字符串ip-整数,主机字节序和网络字节序的转换)

下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:

1
2
3
4
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);//将ip地址转换为网络字节序
int inet_aton(const char *cp, struct in_addr *inp);//将cp转换为网络字节序保存到inp中
char *inet_ntoa(struct in_addr in);//将网络字节序转换为ip字符串

下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

实例(ipv4地址转数字)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <arpa/inet.h>
#include <stdio.h>

int main(){
//将点分十进制的ip转换为整数
char buf[] = "192.168.1.4";
unsigned int num = 0;
inet_pton(AF_INET, buf, &num);
printf("%d\n", num);
unsigned char *p = (unsigned char *)&num;
printf("%d %d %d %d\n", p[0], p[1], p[2], p[3]);
//将整数转换为点分十进制
char ip[16] = {0};
const char *res = inet_ntop(AF_INET, &num, ip, 16);
printf("%s\n", ip);
printf("%d\n", res == ip);
}

TCP通信流程

TCP和UDP的比较

  • UDP:用户数据报协议,面向无连接,可以单播,多播和广播,面向数据报,不可靠
  • TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输

TCP通信基本流程

服务端(Server)

  • 服务端创建一个用于监听的套接字
  • 将监听套接字描述符和本地的ip地址和端口绑定(服务器地址信息)
  • 设置监听,监听的套接字文件描述符开始工作(客户端会向套接字文件缓冲区写数据)
  • 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端连接,会得到一个新的套接字用于通信。

  • 通信:接收数据,发送数据

  • 通信结束,断开连接

客户端(Client)

  • 创建一个用于通信的套接字
  • 连接服务器,需要指定连接的服务器的ip和端口
  • 连接成功,客户端可以直接和服务端通信:接收数据和发送数据
  • 通信结束,断开连接

socket函数

套接字(socket)是一种抽象的概念,它是计算机网络编程中的一种基本单位。它提供了一种标准的接口,供应用程序通过它来与网络通信。

套接字可以理解为一个“端口”,应用程序通过它来与网络进行通信。当应用程序想要向网络发送数据时,它会将数据发送到指定的套接字;当网络向应用程序发送数据时,它也是通过套接字来进行的。

套接字是一个抽象的概念,它并不具体存在,而是由应用程序在运行时创建和使用。它由两个部分组成:一个是套接字地址(socket address),用于标识通信的主机和端口;另一个是套接字类型,用于指定使用的协议。

简而言之,套接字就是一种接口,用于应用程序与网络进行通信。它提供了一种统一的接口,可以让应用程序与网络通信,而不用关心底层的实现细节。

API

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>  
#include <sys/socket.h>
int socket(int domain, int type, int protocol)
//其中,domain参数表示使用的协议族(protocol family),比如AF_INET表示使用IPv4协议,AF_INET6表示使用IPv6协议,AF_UNIX表示使用本地通信协议。
//type参数表示使用的套接字类型(socket type),比如SOCK_STREAM表示使用TCP协议,SOCK_DGRAM表示使用UDP协议。
//protocol参数表示使用的协议,一般情况下可以设置为0,表示使用默认的协议。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//bind 函数接受三个参数:套接字描述符、地址结构体和地址结构体的长度。
//套接字描述符:这是在调用 socket 函数创建套接字时返回的整数值。它用于标识一个特定的套接字。
//地址结构体:这是一个指向 sockaddr_in 结构体的指针,该结构体定义了要绑定到套接字上的网络地址和端口号。
//地址结构体长度:这是 sockaddr_in 结构体的长度。它应该设置为 sizeof(struct sockaddr_in)。
int listen(int sockfd, int backlog);
//这里,sockfd是一个套接字描述符,表示你要监听的套接字,backlog表示请求队列的长度,未连接的和已经连接的。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//接收客户端连接,默认是一个阻塞函数,阻塞等待客户连接
//sockfd: 用于监听的文件描述符
//addr: 传出参数,记录了连接成功后客户端的地址信息
//addrlen: 指定第二个参数的对应的内存
//返回值:成功,返回用于通信的套接字文件描述符,-1失败
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);

简单的C/S模型代码

server

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
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(){
//1. 创建用于监听的socket套接字
//IPV4, 流式通信
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(0);
}
//2. 绑定本地IP地址和端口
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//设置协议族
//设置IP地址
// unsigned int num = 0;
// char buf[] = "127.0.0.1";
// inet_pton(AF_INET, buf, &num);
saddr.sin_addr.s_addr = INADDR_ANY;
//设置端口号,需要转为网络字节序
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1){
perror("bind");
exit(-1);
}

//3. 监听有无客户端连接
ret = listen(lfd, 8);
if(ret == -1){
perror("listen");
exit(-1);
}
//4. 接受客户端的连接
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
if(cfd == -1){
perror("accept");
exit(0);
}

//5. 输出客户端的信息
char client_ip[16];
inet_ntop(AF_INET, (void *)&clientaddr.sin_addr.s_addr, client_ip, 16);
unsigned short client_port = ntohs(clientaddr.sin_port);
printf("client ip is %s, port is %d\n", client_ip, client_port);
//6. 开始通信
char rcv_buf[1024] = {0};
while(1){
int siz = read(cfd, rcv_buf, sizeof(rcv_buf));
if(siz == -1){
perror("read");
exit(-1);
}else if(siz > 0){
printf("rcv client data: %s\n", rcv_buf);
}else if(siz == 0){
//表示客户端断开连接
printf("client closed\n");
break;
}
char *data = "hello, i am server";
write(cfd, data, strlen(data));
}

//7. 关闭连接
close(cfd);
close(lfd);
}

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
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(){

//1. 创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
//2. 连接到server
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1){
perror("connect");
exit(-1);
}
//3. 开始通信
char rcv_buf[1024] = {0};
while(1){
char *data = "hello, i am client";
write(fd, data, strlen(data));
int siz = read(fd, rcv_buf, sizeof(rcv_buf));
if(siz == -1){
perror("read");
exit(-1);
}else if(siz > 0){
printf("rcv server data: %s\n", rcv_buf);
}else if(siz == 0){
//表示客户端断开连接
printf("server closed\n");
break;
}
sleep(1);
}
//4. 关闭套接字
close(fd);
return 0;
}

TCP三次握手

TCP是一种面向连接的单播协议,在发送数据之前,通信双方必须在彼此之间建立一条连接,所谓“连接”,其实是客户端和服务器的内存里保存了一份关于对方的信息。如IP地址和端口号等。

TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包I重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在TCP头部。
TCP提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用四次挥手来关闭一个连接。

三次握手发生在客户端连接的时候,当调用connect函数时底层会通过TCP协议进行三次握手。

  • 客户端向服务端发送数据,其中 SYN = 1
  • 服务端确认收到消息后,发送数据,其中ACK = 1, SYN = 1
  • 客户端向服务端发送数据,其中ACK = 1.

三次握手的目的是为了使得C和S都能确定自己和对方的收发都没问题。

TCP滑动窗口

  • 发送和接受的速度不一致,造成网络拥塞;
  • 可以把窗口理解成缓冲区的大小;
  • 通信的双方都有发送缓冲区和接受缓冲区;
  • 滑动窗口的本质是接收方每次向发送方发送接收方的缓冲区大小(窗口大小)来控制发送方一次可以发送的数据大小以达到拥塞控制的目的。

TCP四次挥手

  • 在通信双方断开连接时进行的操作,客户端和服务端都可以主动发起断开连接
  • 断开方发送FIN=1
  • 接收方发送ACK=1
  • 接收方发送FIN=1
  • 发送方发送ACK=1

TCP通信并发

要实现服务器处理并发的任务,需要使用多线程和多进程来解决。

思路:

一个父进程和多个子进程

  • 父进程负责接受客户端连接
  • 子进程负责通信

简单的多进程C/S实现

server

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
#include <arpa/inet.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>

//回收子进程
void recycleChild(int arg){
while(1){
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1){
break;
}else if(ret == 0){
break;
}else if(ret > 0){
printf("子进程被回收了\n");
}
}
}

int main(){
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recycleChild;

sigaction(SIGCHLD, &act, NULL);

//1. 创建用于监听的socket套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(0);
}
//2. 绑定本地IP地址和端口
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//设置协议族
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1){
perror("bind");
exit(-1);
}
//3. 监听有无客户端连接
ret = listen(lfd, 128);
if(ret == -1){
perror("listen");
exit(-1);
}

//4. 接受客户端的连接
while(1){
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
if(cfd == -1){
if(errno == EINTR) continue;//当回收子进程后,accept不再阻塞,这种情况下需要continue,否则后续client将无法连接
perror("accept");
exit(0);
}

//每一个连接进来,就创建一个子进程
pid_t pid = fork();
if(pid == 0){
//子进程
char client_ip[16];
inet_ntop(AF_INET, (void *)&clientaddr.sin_addr.s_addr, client_ip, 16);
unsigned short client_port = ntohs(clientaddr.sin_port);
printf("client ip is %s, port is %d\n", client_ip, client_port);
char rcv_buf[1024] = {0};
while(1){
int siz = read(cfd, rcv_buf, sizeof(rcv_buf));
if(siz == -1){
perror("read");
exit(-1);
}else if(siz > 0){
printf("rcv client data: %s\n", rcv_buf);
}else if(siz == 0){
//表示客户端断开连接
printf("client closed\n");
break;
}
char *data = "hello, i am server";
write(cfd, data, strlen(data));
}
//7. 关闭连接
close(cfd);
exit(0);//退出当前进程
}
}
close(lfd);
return 0;
}

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
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(){

//1. 创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
//2. 连接到server
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if(ret == -1){
perror("connect");
exit(-1);
}
//3. 开始通信
char buf[1024] = {0};
int i = 0;
while(1){
sprintf(buf, "data: %d\n", i++);
write(fd, buf, sizeof(buf));
int siz = read(fd, buf, sizeof(buf));
if(siz == -1){
perror("read");
exit(-1);
}else if(siz > 0){
printf("rcv server data: %s\n", buf);
}else if(siz == 0){
//表示客户端断开连接
printf("server closed\n");
break;
}
sleep(1);
}
//4. 关闭套接字
close(fd);
return 0;
}

简单的多线程C/S实现

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
#include <arpa/inet.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <wait.h>
#include <pthread.h>

struct sockInfo{
int cfd;//通信的文件描述符
struct sockaddr_in addr;//客户端的信息
pthread_t tid;//线程号
};

//同时支持128个客户端连接
struct sockInfo sock_infos[128];

void *working(void *arg){
//cfd以及客户端的信息以及线程号
struct sockInfo *pinfo = (struct sockInfo *)arg;
char client_ip[16];
inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, client_ip, 16);
unsigned short client_port = ntohs(pinfo->addr.sin_port);
printf("client ip is %s, port is %d\n", client_ip, client_port);
char rcv_buf[1024] = {0};
while(1){
int siz = read(pinfo->cfd, rcv_buf, sizeof(rcv_buf));
if(siz == -1){
perror("read");
exit(-1);
}else if(siz > 0){
printf("rcv client data: %s\n", rcv_buf);
}else if(siz == 0){
//表示客户端断开连接
printf("client closed\n");
break;
}
char *data = "hello, i am server";
write(pinfo->cfd, data, strlen(data));
}
//7. 关闭连接
close(pinfo->cfd);
}

int main(){
//1. 创建用于监听的socket套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(0);
}
//2. 绑定本地IP地址和端口
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//设置协议族
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999);
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1){
perror("bind");
exit(-1);
}
//3. 监听有无客户端连接
ret = listen(lfd, 128);
if(ret == -1){
perror("listen");
exit(-1);
}

for(int i = 0; i < 128; ++i){
bzero(&sock_infos[i], sizeof(sock_infos[i]));
sock_infos[i].cfd = -1;
}

//4. 接受客户端的连接
while(1){
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
if(cfd == -1){
perror("accept");
exit(0);
}

//每一个连接进来,就创建一个子线程
struct sockInfo *pinfo;
for(int i = 0; i < 128; i++){
if(sock_infos[i].cfd == -1){
pinfo = &sock_infos[i];
break;
}
if(i == 127){
sleep(1);
--i;
}
}
pinfo->cfd = cfd;
memcpy(&pinfo->addr, &clientaddr, sizeof(clientaddr));
pthread_create(&pinfo->tid, NULL, working, pinfo);
pthread_detach(pinfo->tid);
}
close(lfd);
return 0;
}

半关闭状态

当TCP链接中A向B发送FIN请求关闭,B回应ACK之后(A进入FIN_WAIT_2状态),并没有立即发送FIN给A, A方则处于半连接状态,此时A可以接收B发送的数据,但是A不能再向B发送数据。从程序的角度,可以使用API来控制半连接状态。

1
2
3
4
5
6
7
#include<sys/socket.h>
int shutdown(int sockfd, int how);
//sockfd: 需要被关闭的socket描述符
//how:
//SHUT_RD(0):关闭sockfd上的读功能
//SHUT_WR(1): 关闭sockfd的写功能
//SHUT_RDWR(2): 关闭sockfd的读写功能,相当于调用shutdown两次

端口复用

  • 端口复用最常用的用途是:防止服务器重启时之前绑定的端口还未释放
  • 程序突然退出而系统未释放端口
1
2
3
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

IO多路复用(I/O多路转接)

I/O多路复用使得程序能够同时监听多个文件描述符,能够提高程序的性能,Linux下实现I/O多路复用的系统调用主要有select、poll和epoll。

常见I/O模型

  • 阻塞等待:优点是不占用宝贵的CPU时间片,缺点是同一时刻只能处理一个操作。
  • 非阻塞,忙轮询:要使用IO多路复用技术。

1. select

主旨思想:

  • 构建一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中
  • 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行了IO操作,该函数才返回(该函数是阻塞的)
  • 在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/select.h>
#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);
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
//nfds: 委托内核检测的最大的文件描述符的值+1
//readfds: 要检测的读的文件描述符的集合,委托内核检测哪些文件描述符的读缓冲区,一般只检测读操作(对方发送过来的数据),是一个传入传出参数
//writefds: 要检测的写的文件描述符的集合,判断是否有剩余空间
//exceptfds: 检测异常的文件描述符的集合,一般不会用
//timeout: NULL为永久阻塞,直到检测到了文件描述符有变化,否则根据timeout设置的时间进行阻塞
//返回值:-1失败, n:有n个文件描述符发生了变化
void FD_CLR(int fd, fd_set *set);//标志位fd设置为0
int FD_ISSET(int fd, fd_set *set);//判断是否是1
void FD_SET(int fd, fd_set *set);//标志位fd设置为1
void FD_ZERO(fd_set *set);//清空set

程序实例

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
#include <arpa/inet.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
#include <sys/select.h>
#include <sys/time.h>

int main(){
int lfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
char pip[] = "127.0.0.1";
inet_pton(AF_INET, pip, &saddr.sin_addr.s_addr);

bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

listen(lfd, 8);

//创建一个fdset和tempset,存放需要检测的文件描述符,可以表示1024个文件描述符
fd_set rdset, tempset;
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
int maxfd = lfd;
while(1){
tempset = rdset;
//调用select让内核检测哪些文件描述符有数据
int ret = select(maxfd + 1, &tempset, NULL, NULL, NULL);
if(ret == -1){
perror("select");
exit(-1);
}else{
//检测到了有文件描述符的数据发生了改变
if(FD_ISSET(lfd, &tempset)){
//有新的客户端连接进来
struct sockaddr_in clientaddr;
int len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
//将新的文件描述符加入到集合中
FD_SET(cfd, &rdset);
//更新最大的文件描述符
maxfd = maxfd > cfd ? maxfd : cfd;
}

//遍历文件描述符
for(int i = lfd + 1; i <= maxfd; i++){
if(FD_ISSET(i, &tempset)){
//说明i中有数据, 通信
char buf[1024] = {0};
int len = read(i, buf, sizeof(buf));
if(len == -1){
perror("read");
exit(-1);
}else if(len == 0){
printf("client closed ...\n");
close(i);
FD_CLR(i, &rdset);
}else if(len > 0){
printf("received buf = %s\n", buf);
write(i, buf, strlen(buf) + 1);
}
}
}
}
}
close(lfd);
return 0;
}
  • select需要每次将文件描述符数组从用户态拷贝到内核态
  • O(n) 复杂度的遍历
  • select支持的文件描述符太少,默认是1024
  • 文件描述符不能重用,每次都要重置

2. poll

poll和select用法大致相同,但是使用了pollfd结构体来封装文件描述符

1
2
3
4
5
6
7
8
9
#include <poll.h>
struct pollfd{
int fd;
short events;//POLLIN, POLLOUT...
short revents;
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//timeout: -1为阻塞,0为不阻塞,>0为阻塞时长
//返回值:如果是-1表示失败,如果>0:表示成功,表示检测到集合中有n个文件描述符发生变化

实例代码

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

#include <poll.h>

struct pollfd pf[1024];

void add(int fd){
for(int i = 0; i < 1024; i++){
if(pf[i].fd == -1){
pf[i].fd = fd;
return ;
}
}
}
void work(char *buf){
for(int i = 0; i < strlen(buf); i++){
if(buf[i] >= 'a' && buf[i] <= 'z'){
buf[i] -= ('a' - 'A');
}
}
}
int main(){
for(int i = 0; i < 1024; i++){
pf[i].fd = -1;
pf[i].events = POLLIN;
}
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1){
perror("socket");
exit(-1);
}

struct sockaddr_in sockaddr;
sockaddr.sin_family = AF_INET;
sockaddr.sin_port = htons(9999);
char ip[] = "127.0.0.1";
inet_pton(AF_INET, ip, &sockaddr.sin_addr.s_addr);

int ret = bind(lfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
if(ret == -1){
perror("bind");
exit(-1);
}

ret = listen(lfd, 8);
if(ret == -1){
perror("listen");
exit(-1);
}
add(lfd);
char buf[1024] = {0};
while(1){
ret = poll(pf, 1024, -1);
if(ret == -1){
perror("poll");
exit(-1);
}else if(ret == 0){
continue;
}else{
if(pf[0].revents & POLLIN){//表示有新的连接进来
struct sockaddr_in cliaddr;//客户端地址
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
add(cfd);
}

//遍历pf进行通信
for(int i = 1; i < 1024; i++){
if(pf[i].revents == POLLIN){
int len = read(pf[i].fd, buf, sizeof(buf));
if(len == -1){
perror("read");
exit(0);
}
if(len == 0){
printf("client %d is closed\n", pf[i].fd);
pf[i].fd = 0;
pf[i].events = 0;
close(pf[i].fd);
}else if(len > 0){
printf("received from client %d: %s\n", pf[i].fd, buf);
work(buf);
write(pf[i].fd, buf, strlen(buf));
}
}
}
}
}
close(lfd);
return 0;
}

3. epoll

  • 调用epoll_create创建一个epoll实例(在内核区,不需要每次从用户区拷贝到内核区)
  • 使用红黑树遍历,效率更高

  • API

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
#include <sys/epoll.h>
//创建一个epoll实例。在内核中开辟一块数据空间,这个数据中两个比较重要的部分是一个是需要检测的文件描述符(红黑树),还有一个是就绪列表。存放检测到数据发生改变的文件描述符信息。
int epoll_create(int size);
-参数size:没什么意义,大于0即可
-返回值:-1是失败,>0则返回一个操作epoll实例的文件描述符
//修改epoll实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-epfd: epoll实例的文件描述符
-op: 要进行什么操作
-EPOLL_CTL_ADD
-EPOLL_CTL_MOD
-EPOLL_CTL_DEL
-fd:要检测的文件描述符
-event: 检测文件描述符什么事件
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-epfd:epoll实例对应的文件描述符
-events: 传出参数:保存了发生变化的文件描述符的信息
-maxevents: 第二个参数结构体数组的大小
-timeout: 阻塞时间
0: 不阻塞
-1: 阻塞,直到fd数据发生变化
>0: 阻塞时长
-返回值:
成功:返回发生变化的文件描述符的数目
失败:返回-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
    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
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <sys/epoll.h>

    int main(){
    //创建socket
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
    perror("socket");
    exit(-1);
    }
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    char ip[] = "127.0.0.1";
    inet_pton(AF_INET, ip, &saddr.sin_addr.s_addr);

    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1){
    perror("bind");
    exit(-1);
    }
    ret = listen(lfd, 8);

    //创建一个epoll实例
    int epfd = epoll_create(100);
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
    struct epoll_event epevs[1024];
    while(1){
    ret = epoll_wait(epfd, epevs, 1024, -1);
    if(ret == -1){
    perror("epoll wait");
    exit(-1);
    }
    printf("ret = %d\n", ret);
    for(int i = 0; i < ret; i++){
    int curfd = epevs[i].data.fd;
    if(curfd == lfd){
    //监听的文件描述符
    struct sockaddr_in cliaddr;
    int len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

    epev.events = EPOLLIN;
    epev.data.fd = cfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
    }else{
    char buf[1024] = {0};
    int len = read(curfd, buf, sizeof(buf));
    if(len == -1){
    perror("read");
    exit(-1);
    }else if(len == 0){
    printf("client closed\n");

    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
    close(curfd);
    }else if(len > 0){
    printf("read buf = %s\n", buf);
    write(curfd, buf, strlen(buf) + 1);
    }
    }
    }
    }
    close(lfd);
    close(epfd);
    return 0;
    }
    • LT模式

      。。。

    • ET模式

      。。。

UDP 通信实现

API

1
2
3
4
5
6
7
8
9
10
#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);
//sockfd: 通信的fd
//buf: 发送的数据
//len: 发送数据的长度
//flags: 0
//dest_addr: 通信另一端的地址
//addrlen: 地址的大小
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

代码实例(server & 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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(){
//1. 创建UDP套接字,参数为SOCK_DGRAM
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd == -1){
perror("socket");
exit(-1);
}
//2. 绑定本地ip & 端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
char ip[] = "127.0.0.1";
inet_pton(AF_INET, ip, &addr.sin_addr.s_addr);

int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1){
perror("bind");
exit(-1);
}

//3. 通信

printf("server started...\n");
while(1){
char buf[1024];
struct sockaddr_in client_addr;
int len = sizeof(client_addr);
int num = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &len);
char ipbuf[128];
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf, sizeof(ipbuf));
printf("client ip: %s, client port : %d\n", ipbuf, ntohs(client_addr.sin_port));
printf("client data: %s\n", buf);
sendto(fd, buf, strlen(buf), 0, (struct sockaddr *)&client_addr, len);
}
close(fd);
}
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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(){
//1. 创建通信套接字
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd == -1){
perror("socket");
exit(-1);
}

//2. 设置连接地址端口信息
struct sockaddr_in server_ip;
server_ip.sin_family = AF_INET;
char ip[] = "127.0.0.1";
inet_pton(AF_INET, ip, &server_ip.sin_addr.s_addr);
server_ip.sin_port = htons(9999);

//3. 通信

while(1){
char buf[1024];
fgets(buf, sizeof(buf), stdin);
sendto(sfd, buf, strlen(buf), 0, (struct sockaddr *)&server_ip, sizeof(server_ip));

struct sockaddr_in addr;
int len = sizeof(addr);
recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr *)&addr, &len);

printf("received from server: %s\n", buf);
}
close(sfd);
}