Linux网络编程

Linux网络编程

一、网络编程概念

1.mac地址

标识网卡的id,理论上这个id全球唯一

mac地址一般用来标识主机的id,这个id是物理地址,不会改变

2. IP地址

IP地址是标识主机的id,这个id是虚拟的,会改变的。

一个IP将其分为子网id和主机id,子网id和主机id需要和子网掩码一起看,比如说下面有一个IP地址和子网掩码

1
2
10.1.1.2
255.255.255.0

上面的192.168.11.23是IP地址,而下面的255.255.255.0是子网掩码,查看时需要看下面的子网掩码。

IP中被连续的1覆盖的位就是子网id

IP中被连续的0覆盖的位就是主机id

所以子网id是:10.1.1

主机id是:2

网段地址:10.1.1.0

广播地址:10.1.1.255

主机id分配的范围:10.1.1.1->10.1.1.254

ping:这是一个用来测试两台主机的网络联通性的命令

ens33是网络名称

1
2
192.168.131.133 设置的ip
netmask子网掩码255.255.255.0

3.端口

作用:用来标识应用程序(进程)

port:2个字节 0-65535

0-1023知名端口

自定义端口1024-65535

4.OSI七层模型

物理层:双绞线接口类型,光纤的传输速率等等

数据链路层:mac 负责收发数据

网络层:IP 给两台提供路径选择

传输层:port 区分数据递送到哪一个应用程序

会话层:建立连接

表示层:解码

应用层:应用程序,拿到数据

TCP/IP四层协议

5.协议

规定了数据传输的方法和格式

应用层协议:
FTP:文本传输协议

HTTP:超文本传输协议

NFS:网络文件系统

传输层协议:

TCP:传输控制协议

UDP:用户数据包协议

网络层协议:

IP:因特网互联协议

ICMP:因特网控制报文协议,比如ping

IGMP:因特网组管理协议

链路层协议:
ARP:地址解析协议 通过IP找mac地址

RARP:反向地址解析协议,通过mac找IP

mac头部:

6.网络通讯过程

7.arp

地址解析协议:通过IP找mac地址

arp请求包:

8.网络设计模式

有两种设计模式

B/S browser/server 使用服务器进行计算

优点:客户端安全,开发周期短

缺点:性能低

C/S cilent/server 使用客户端就行计算

优点:性能好

缺点:客户端容易篡改数据,开发周期较长

9.进程间通讯

之前学习了一些进程间的通讯,比如无名管道、有名管道、mmap、文件、信号、消息队列、共享内存,但这些通讯都存在一个问题就是只能用于本机的进程间通讯。

如果我们想让不同的主机之间进行通讯,我们需要使用到 socket。

10.三次握手

在TCP通讯的时候会需要进行三次握手,当上次握手结束后就会建立TCP的通讯。一般使用在连接。

这个过程和我们打电话的过程是一样的,当你打了一个电话给对方首先需要确定一下对方是不是接通了,就得先说一声喂,对方收到你的喂后也会回复一个喂,接收到这个喂之后再进行一个确认就可以与对方进行通话了。

三次握手的示意图如下:

当客户端连接服务器的时候会先发送一个数据包,数据包中的SYN位会置为1,当服务器接收到这个数据包后也会发送一个数据包给客户端,告诉客户端我已经接收到你的连接,而这个数据包中的ACK会置为1,当客户端接收到这个服务器的数据包后又会再发送一个数据包,这个数据包中的SYN会置为0,发送完这个数据包后就可以开始通讯了。

而每个数据包中都有一个序列号seq和确定序列号ack,序列号是拿来表示发送的数据包的序号的,而确定序列号的含义有两条:

  • 确定收到对方的数据包
  • 期待下一次对方的序列号为我的确定序列号

这里的握手必须得是三次,因为在传输的过程中很有可能会发生传输了一个数据包但是延迟比较高的情况,如果是只握两次手,那么第一次扔包过去的时候可能时间会很长,那么客户端又会扔一个数据包,这个数据包一下子就扔到了服务器上,然后服务器马上就返回一个确定数据包,然后客户端就开始通讯了,在通讯的时候,第一次扔的包成功的给到了服务器,服务器又以为要建立一个行的连接,这个时候就会产生另外一个连接队列,但本质上两个队列都是一样的。

而如果是三次握手,客户端已经确定好连接了,当那个包又回来时,服务器就会自动忽略那个数据包,就不会再创建一个连接队列了。

11.四次挥手

四次挥手一般出现在关闭连接的时候。

这个过程可以理解为你和别人打电话结束后要挂电话的情况,你要挂电话你就得说我要挂了,对方会回复说好的,然后就会说我也要挂掉电话了,然后你回复好的,就可以把电话挂了。

四次挥手的示意图如下:

主动方执行close时,就会给被动方一个数据包,这个数据包中FIN置为1,然后被动方接收到这个关闭数据包后也会回复一个数据包,这个数据包中ACK为1,FIN为0,紧接着再发送一个挂掉的数据包,也就是close,这个数据包和主动方第一次发送的包一样,然后等待主动方发送挂掉的数据包,接收到这个数据包后就可以结束连接了。

而这个主动和被动可以是客户端主动,服务器被动,也可以是服务器主动,客户端被动,这个无所谓的

而最大报文生存时间是被动方需要等待的,不管被动方是哪一个。

在这个过程中虽然调用了close,但是还是可以收发数据的,这个过程叫做半关闭,半关闭的函数后面会说

12.mss

mss是最大报文长度,一般出现在三次握手的前两次,用来告诉对方发送数据的最大长度。

13.MTU

网卡的最大传输单元

14.2MSL

为了让4次握手关闭流程更加可靠。还有其它功能,但这里只了解这个问题即可。

14.滑动窗口

每一次读取数据之后,回ack报文,报文中携带当前缓冲区大小,用来告知对方我缓冲区的空间。

15.TCP转换图

TCP转换图其实如下

本质上就是在建立连接和断开连接的过程中主动方和被动方的一些标志位

二、Socket编程

1.套接字概念

套接字其实就是一个插座,是计算机之间进行通讯的一种约定或一种方法。

2.预备知识

2.1 网络字节序

这里需要重新回忆一下C语言中的大端存储和小端存储了。其实也就是每个人的计算机的字节序存储方式不一样,有些是大端存储,而有些是小端存储,如果直接进行通讯就像中国人和英语人交流信笺一样,两边的人都看不懂对象书写的内容,这个时候如果在写信的时候将自己写的内容转换成对方看得懂的内容是不是就能很好的解决看不懂的问题了。

为了使网络程序具有可移植性,使同样的C代码在大端计算机和小端计算机上编译后都能正常运行,就可以使用以下库函数做网络字节序和主机字节序的转换

主机字节序转换为网络字节序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
功能:
将无符号整数hostlong从主机字节序转换为网络字节序
参数:
hostlong:需要转换的主机字节序
返回值:
转换后的网络字节序
uint16_t htons(uint16_t hostshort);
功能:
将无符号短整型hostshort从主机字节序转换为网络字节序
参数:
hostshort:需要转换的主机字节序
返回值:
转换后的网络字节序

网络字节序转换成主机字节序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <arpa/inet.h>
uint32_t ntohl(uint32_t netlong);
功能:
将无符号短整数netlong转换为主机字节序
参数:
netlong:需要转换的网络字节序
返回值:
转换后的主机字节序
uint16_t ntohs(uint16_t netshort);
功能:
将无符号短整数netshort转换为主机字节序
参数:
netshort:需要转换的网络字节序
返回值:
转换后的主机字节序

示例程序:

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

int main(){
//将长主机字节序转换为网络字节序
unsigned char buf[] = {192, 168, 1, 2};
unsigned int num = *((unsigned int*)buf);
unsigned int sum = htonl(num);
unsigned char* p = (unsigned char*)&sum;
printf("%d.%d.%d.%d\n", *p, *(p+1), *(p+2), *(p+3));
//将短主机字节序转换为网络字节序
unsigned int a = 0x0102;
unsigned int b = htons(a);
printf("%x\n", b);
//将长网络字节序转换为主机字节序
unsigned int c = ntohl(sum);
unsigned char* pc = (unsigned char*)&c;
printf("%d.%d.%d.%d\n", *pc, *(pc+1), *(pc+2), *(pc+3));
//将短网络字节序转换为主机字节序
unsigned int d = htons(b);
printf("%x\n", d);
return 0;
}

2.2 IP地址转换函数

当你选择已经有一个需要发送的IP:192.168.1.2,你如果直接发送,网络是没办法知道你这个是什么内容的,需要进行一次转换将192.168.1.2转换为192,168,1,2这种使用数组存放的形式。而网络传给主机的IP地址为192,168,1,2这种使用数组的IP地址,所以需要经过转换变成192.168.1.2的形式。

在C语言中提供了两个函数来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <arpa/inet.h>
int inet_pton(int af, const char* src, void* dst);
功能;
将点分十进制串转成32位网络大端的数据
参数:
af:
AF_INET IPV4
AF_INET6 IPV6
src:点分十进制串的首地址
dst:32位网络数据的地址
返回值:
成功:返回1(网络地址已成功连接)
失败:0(src 不包含表示指定地址系列中有效网络地址的字符串)
-1(af 不包含有效的地址系列)
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <arpa/inet.h>
const char* inet_ntop(int af, const void* src, char* dst, socklen_t size);
功能:
32位大端的网络数据转成点分十进制
参数:
af:
AF_INET IPV4
AF_INET6 IPV6
src:32位大端网络数地址
dst:存储点分十进制串地址
size:存储点分制串数组的大小
返回值:
存储点分制串数组首地址

示例程序:

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

int main(){
unsigned char buf[] = "192.168.1.2";
unsigned char ip[16];
unsigned int number = 0;
unsigned char* p = NULL;
inet_pton(AF_INET, buf, &number);
p = (unsigned char*)&number;
printf("%d %d %d %d\n", *p, *(p+1), *(p + 2), *(p + 3));
inet_ntop(AF_INET, &number, ip, 16);
printf("%s\n", ip);
return 0;
}

2.3 sockaddr数据结构

ipv4套接字结构体

1
2
3
4
5
6
7
8
9
10
struct sockaddr_in {
sa_family_t sin_family; /* 地址族: AF_INET */
u_int16_t sin_port; /* 按网络字节次序的端口 */
struct in_addr sin_addr; /* internet地址 */
};

/* Internet地址. */
struct in_addr {
u_int32_t s_addr; /* 按网络字节次序的地址 */
};

ipv6套接字结构体

1
2
3
4
5
6
7
8
9
10
11
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};

struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};

通用套接字结构体

1
2
3
4
struct sockaddr{
sa_family_t sa_family; /*AF_xxx*/
char sa_data[14]; /*通用的地址*/
}

3.网络套接字函数

3.1 socket模型创建流程图

3.2 socket函数

创建套接字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:
建立一个用于交流的端点并且返回一个描述符
参数:
domain:AF_INET
type:确定通信语句
SOCK_STREAM 提供有序的,可靠的,双向的,基于字节流的通讯。可能支持带外传输。
SOCK_DGRAM 提供数据报(不面向连接的, 不可靠的固定最大长度的信息)。
SOCK_SEQPACKET 提供有序的,可靠的,双向的,基于固定最大长度的数据报传输路径;需要一个读取整个伴有输入系统调用的包的用户。
SOCK_RAW 提供未加工(raw)的网络协议通道。
SOCK_RDM 提供可靠的数据报层,但是不保证顺序。
SOCK_NONBLOCK 设置 O_NONBLOCK 的标志于新打开的文件描述符。 通过这个标志可以不用调用 fcntl(2) 来达到相同的结果。
SOCK_CLOEXEC 设置 close-on-exec (FD_CLOEXEC) 的标志于新打开的文件描述符。参见 open(2) 中关于 O_CLOEXEC 的描述,因为一些原因这个标志很有用。
protocol:指定一个协议用于套接字,一般为0
返回值:
成功:文件描述符
失败:-1

3.3 connect函数

连接服务器

1
2
3
4
5
6
7
8
9
10
11
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
功能:
连接服务器的函数
参数:
sockfd:套接字文件描述符
addr:ipv4或者ipv6的结构体,但需要转换为通用结构体
addrlen:结构体的大小。
返回值:
成功: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
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(){
int sock_fd, n;
struct sockaddr_in sa;
//创建套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
//连接主机
sa.sin_family = AF_INET;
sa.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.160.1", &sa.sin_addr.s_addr);
connect(sock_fd, (struct sockaddr*)&sa, sizeof(sa));
//发送内容
char buf[1024];
while(1){
read(STDIN_FILENO, buf, sizeof(buf));
write(sock_fd, buf, sizeof(buf));//发送数据给服务器
n = read(sock_fd, buf, sizeof(buf));
write(STDIN_FILENO, buf, n);
}
//关闭文件描述符
close(sock_fd);
return 0;
}

3.4 tcp服务器通信流程

3.5 bind绑定

1
2
3
4
5
6
7
8
9
10
11
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
功能:
给套接字绑定端口和ip
参数:
sockfd:套接字
addr:ipv4套接字结构体地址
addrlen:ipv4套接字结构体的大小
返回值:
成功0
失败-1

3.6 listen

1
2
3
4
5
6
7
8
9
10
#include <sys/socket.h>
int listen(int s, int backlog);
功能:
在一个套接字上设置倾听连接
参数:
s:套接字
backlog:已完成连接队列和未完成连接队列数值和的最大值 128
返回值:
成功:0
失败:-1,并设置相应错误代码

3.7 accept

1
2
3
4
5
6
7
8
9
10
11
#include <sys/socket.h>
int accept(int socket, struct sockaddr* address, socklen_t* addrlen);
功能:
在一个套接字上接收一个连接,如果连接队列没有新的连接,accept回阻塞
参数:
socket:套接字
address:获取客户端的ip和端口信息 ipv4套接字结构体地址
addrlen:IPv4套接字结构体的大小的地址
返回值:
成功:新的已连接套接字的文件描述符
失败:-1

这里要说明一下,accept很任意被系统的中断给关闭,所以在使用accept的时候我们需要添加一个判断,并且要过滤系统中断

1
2
3
4
5
6
7
8
9
10
11
agare:
ret = accept(sock, &ipv4, &ipv4_len);
if (ret == -1){
if ((errno == ECONNABORTED) || (errno == EINTR)){
goto agare;
}
else{
perror("accept");
return 0;
}
}

3.8 tcp服务器通信步骤

1.创建套接字 socket

2.绑定 bind

3.监听 listen

4.提取 accept

5.读写

6.关闭

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

#define SIZE 1024

int main(){
int sock_fd, new_sock_fd, ret = -1, size, n;
struct sockaddr_in sa;
char buf[SIZE];
sa.sin_family = AF_INET;
sa.sin_port = htons(8000);
//inet_pton(AF_INET, "192.168.160.132", &sa.sin_addr.s_addr);
sa.sin_addr.s_addr = INADDR_ANY;//绑定的是通配地址
//1.创建套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == SO_ERROR){
perror("socket");
return 1;
}
//2.绑定bind
ret = bind(sock_fd, (struct sockaddr*)&sa, sizeof(sa));
if (ret == SO_ERROR){
perror("bind");
return 1;
}
//3.监听listen
ret = listen(sock_fd, 128);
if (ret == SO_ERROR){
perror("listen");
return 1;
}
size = sizeof(sa);
//4.提取accept
new_sock_fd = accept(sock_fd, (struct sockaddr*)&sa, &size);
if (new_sock_fd == -1){
perror("accept");
return 1;
}
printf("hello, word\n");
//5.读写
while(1){
n = read(STDIN_FILENO, buf, sizeof(buf));
write(new_sock_fd, buf, SIZE);
memset(buf, 0, SIZE);
n = read(new_sock_fd, buf, SIZE);
printf("He speak:%s\n", buf);
memset(buf, 0, SIZE);
}
//7.关闭
close(new_sock_fd);
close(sock_fd);
return 0;
}

这里可以利用read读取客户端发送的数据长度来判断客户端是否关闭,当read读取的内容为0,就可以判断客户端关闭。

3.9 tcp服务器多客户端连接(进程版)

3.8中的只能连接一个客户端,但如果我们这个服务端要连接多个客户端,这个时候就需要使用到之前学过的进程方面的知识点了,过程如下:

主进程连接队列,子进程处理进入已连接队列的套接字

按照这样设计,那么主进程中就可以不需要已连接队列的操作,子进程就可以不需要未连接队列。

代码设计如下:

1
2
3
4
5
6
1.创建套接字
2.绑定端口
3.监听
4.提取连接
5.创建进程
6.关闭

代码如下:

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

void fun1(int signum, siginfo_t* info, void* context){
int p;
while(1){
p = waitpid(-1, NULL, WNOHANG);
if (p <= 0){
//没有子进程要退出
break;
}
printf("信号为:%d\n子进程退出\n", signum);
}
}

int main(){
int sock = -1, ret = -1, forkNumber = -1, new_sock = -1, ipv4_len;
struct sockaddr_in ipv4, client_ipv4;
//设置一个阻塞集,避免在接收信号之前就有这个信号
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
//1.创建套接字
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1){
perror("socket");
return 0;
}
//2.绑定端口
ipv4.sin_family = AF_INET;
ipv4.sin_port = htons(8000);
inet_pton(AF_INET, "192.168.40.131", &ipv4.sin_addr.s_addr);
ret = bind(sock, (struct sockaddr*)&ipv4, sizeof(ipv4));
if (ret == -1){
perror("bind");
return 0;
}
//3.监听
ret = listen(sock, 128);
if (ret == -1){
perror("listen");
return 0;
}

while(1){

//5.提取连接
char ip[16];
ipv4_len = sizeof(ipv4);
again:
new_sock = accept(sock, (struct sockaddr*)&ipv4, &ipv4_len);
if (new_sock < 0){
if ((errno == ECONNABORTED) || (errno == EINTR)){
goto again;
}
else
perror("accept");
}
printf("new client ip:%s port:%d\n", inet_ntop(AF_INET, &ipv4.sin_addr.s_addr, ip, 16), ntohs(ipv4.sin_port));
if (new_sock == -1){
perror("accept");
exit(0);
}
//4.创建进程
forkNumber = fork();
if (forkNumber == -1){
perror("fork");
exit(0);
}
else if (forkNumber == 0){
//关闭等待队列
char buf[1024] = "";
ret = close(sock);
int n;
//对就绪的套接字进行操作
while(1){
n = read(new_sock, buf, 1024);
if (n == 0){
printf("new client ip:%s port:%d is close!\n", inet_ntop(AF_INET, &ipv4.sin_addr.s_addr, ip, 16), ntohs(ipv4.sin_port));
exit(0);
}
else if (n == -1){
//客户端退出
printf("new client ip:%s port:%d is close!\n", inet_ntop(AF_INET, &ipv4.sin_addr.s_addr, ip, 16), ntohs(ipv4.sin_port));
exit(0);
}
else{
printf("%s\n", buf);
write(new_sock, buf, n);
}
}
}
else{
//父进程
//关闭就绪队列
ret = close(new_sock);
while(ret == -1){
perror("close");
ret = close(new_sock);
}
//当子进程结束后,会产生一个信号
//6.关闭
struct sigaction act;
act.sa_sigaction = fun1;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
return 1;
}

3.10 tcp服务器多客户端连接(线程版)

其实设计的逻辑和进程的差不多

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

#define PORT 9999
#define IP "192.168.18.128"
#define SIZE 512

struct SockMessage{
int sockfd;
struct sockaddr_in clientaddr;
};

void* recvThread(void* sockfd){
char buf[SIZE];
struct SockMessage message = *((struct SockMessage*)sockfd);
int n;
// 用来接收客户端传过来的信息
while(1){
n = recv(message.sockfd, buf, SIZE, 0);
if (n < 0){
printf("fail not recv\n");
}
else if (n == 0){
printf("The client quited\n");
pthread_exit(NULL);
}
if (strcmp(buf, "exit") == 0){
printf("[%s %d] is exit\n", inet_ntoa(message.clientaddr.sin_addr), ntohs(message.clientaddr.sin_port));
pthread_exit(NULL);
}
fflush(stdout);
printf("[%s %d]:%s\n", inet_ntoa(message.clientaddr.sin_addr), ntohs(message.clientaddr.sin_port), buf);
memset(buf, 0, SIZE);
}
}

int main(){
int sockfd = -1;
struct sockaddr_in serveraddr = {0};
struct sockaddr_in clientaddr = {0};
int len = sizeof(struct sockaddr);
int new_sockfd = -1;
int on = 1;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1){
perror("fail not socket");
return -1;
}
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(PORT);
serveraddr.sin_addr.s_addr = inet_addr(IP);
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)) == -1){
perror("fail not bind");
close(sockfd);
return -2;
}
if (listen(sockfd, 8) == -1){
perror("fail not listen");
close(sockfd);
return -3;
}
while(1){
pthread_t pid = -1;
struct SockMessage message;
// 接收连接
new_sockfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if (new_sockfd < 0){
perror("fail not accept");
close(sockfd);
return -3;
}
printf("Get new link:[%s %d]\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
message.sockfd = new_sockfd;
message.clientaddr = clientaddr;
// 创建线程
if (pthread_create(&pid, NULL, recvThread, (void*)&message) != 0){
perror("fail to pthread_create");
}
pthread_detach(pid);
}
return 0;
}

4.半关闭

在讲四次握手的时候说了,当调用了close后,并没有完全关闭,而会处于一种半关闭的情况,也就是主动方发生在FIN_WAIT_2状态时,主动方不可以在应用层发送数据,但是应用层还可以接收数据。

1
2
3
4
5
6
7
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd:需要关闭的socket的描述符
how:允许为shutdown操作选择一下几种方法:
SHUT_RD(0):关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。该套接字**不再接收数据**,任何当前在套接字接收缓冲区的数据将被无声丢弃掉
SHUT_WR(1):关闭sockfd的写功能,此选项将不允许sockfd进行写操作,进程不能在对此套接字发出写操作
SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后是SHUT_WR。

5.心跳包

如果对方异常断开,本机检测不到,一直等待会浪费资源

所以这里可以设置一个心跳包来检测对方是不是断开了,这里的原理其实就是每隔一定的时间间隔就发送一个探测分节,如果连续发送多个探测分节对方未回,就将次连接断开

1
2
int keepAlive = 1;
setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, (void*)&keepAlive, sizeof(keepAlive));

6.设置端口复用

在有些时候操作服务端时,服务端强制退出了,这个时候还没有反应过来就会导致这个端口还在占用,需要再过一段时间才会释放调这个端口的资源,那如果我们还想继续使用这个端口就得等这个端口的释放,但是我们可以设置端口的复用来解决这个问题

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

注意:程序中设置某个端口重新使用,在这之前的其它网络程序将不能使用这个端口

三、高并发服务器

之前学会写了一个服务器,但之前写的服务器只是阻塞等待的服务器,也就是说来了一个客户端,服务器就需要创建一个进程或者线程去连接那个服务器,这样是很消耗资源的。

而还有一种服务器是非阻塞忙轮服务器,这个服务器是创建出一些进程,然后CPU就会去遍历这些进程,看看这些进程有没有空闲的,如果有客户端需要连接,CPU就会让没有连接的线程去连接,这种方法比较消耗CPU。

最后一种就是我们要讲的了,就是多路IO,多路IO这种方法是利用这内核的中断,当一个线程空闲后,内核就会产生一个信号给CPU,让CPU知道有进程空闲。

多路IO有三种监听的方式:pollepollselect

它是用内核监听多个文件描述符的属性(读写缓冲区)变化,如果某个文件描述符的读缓冲区变化了,这个时候就是可以读了,将这个事件告知应用层。

1.select

在Windows中比较采用,而且能跨平台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
功能:监听多个文件描述符的属性变化(读写异常)
void FD_CLR(int fd, fd_set* set); // 将fd文件描述符从set集合中删除
int FD_ISSET(int fd, fd_set* set); // 判断fd文件描述符是否在set集合中
void FD_SET(int fd, fd_set* set); // 将fd描述符添加到set集合中
void FD_ZERO(fd_set* set); // 将set集合中的文件描述符清空
参数:
nfds:最大文件描述符+1
readfds:需要监听的读的文件描述符存放集合
writefds:需要监听的写的文件描述符存放集合 NULL
exceptfds:需要监听的异常的文件描述符存放集合 NULL
timeout:多长时间监听一次 固定的时间,限时等待 NULL 永久监听
struct timeval{
long tv_sec://秒
long tv_usec;//微秒
}
返回值:
返回的是变化的文件描述符的个数
注意:变化的文件描述符会存放在监听的集合中,未变化的文件描述符会从集合中删除

1.1 使用select写TCP服务器

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

#define PORT 8888 // 绑定的端口

int main(){
struct sockaddr_in server;
struct sockaddr_in client; // 链接的客户端
int ret;
int maxfd;
fd_set oldset, reset;
int lenfd; // 存放变化的个数
int cfd;
int clientlen;

// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0){
perror("socket");
return -1;
}
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
inet_pton(AF_INET, "127.0.0.1", &server.sin_addr.s_addr);
// 绑定套接字
ret = bind(lfd, (struct sockaddr*)&server, sizeof(struct sockaddr));
if (ret < 0){
perror("bind");
return -2;
}
// 监听
ret = listen(lfd, 128);
if (ret < 0){
perror("listen");
return -3;
}
maxfd = lfd;
// 将集合中的内容清空
FD_ZERO(&oldset);
FD_ZERO(&reset);
// 将lfd放进oldset中
FD_SET(lfd, &oldset);
while(1){
reset = oldset;
lenfd = select(maxfd + 1, &reset, NULL, NULL, NULL);
if (lenfd < 0){
perror("select");
break;
}
else if (lenfd == 0){
printf("bbb");
continue;
}
else{
if (FD_ISSET(lfd, &reset)){
// lfd变化了,接受cfd
clientlen = sizeof(client);
cfd = accept(lfd, (struct sockaddr*)&client, &clientlen);
if (cfd < 0){
// 接收失败
perror("accept");
continue; // 跳过这次接收
}
printf("link!client IP:%s port:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// 接收成功将这个文件描述符添加到oldset中
FD_SET(cfd, &oldset);
// 判断一下这个描述符是否大于最大文件描述符
if (cfd > maxfd){
maxfd = cfd;
}
// 如果只有lfd变化那就跳过这次
if (--lenfd == 0)
continue;
}
for (int i = lfd + 1; i <= maxfd; i++){
// 判断哪个文件描述符在变化集合中
if (FD_ISSET(i, &reset)){
char buf[1500];
ret = read(i, buf, 1500);
if (ret < 0){
// 这个文件描述符有问题
perror("read");
// 关闭这个文件描述符
close(i);
// 然后把这个文件描述符从中删除
FD_CLR(i, &oldset);
continue;
}
else if (ret == 0){
// 这个文件描述符已经关闭
close(i);
FD_CLR(i, &oldset);
}
else{
// 正常输出
printf("%s\n", buf);
write(i, buf, strlen(buf));
bzero(buf, 0);
}
}
}
}
}
return 0;
}

1.2 select的优缺点

优点:跨平台

缺点:

文件描述符1024的限制,由于FD_SETSIZE的限制,只能返回变化的文件描述符的个数,具体哪个变化需要遍历,每次都需要将需要监听的文件描述符集合由应用层拷贝到内核。

大量并发,少活跃,select效率低。

假设现在有4-1023个文件描述符需要监听,但是5-1000这些文件描述符关闭了。

1.3 解决缺点中的问题

这里使用到数组来进行解决。

2.poll

用得比较少

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:监听多个文件描述符的首元素地址的属性变化
参数:
fds:监听的数组的首元素地址
nfds:数组有效元素的最大下标+1
timeout:超时时间,-1是永久监听,>=0限时等待
数组元素:
struct pollfd{
int fd; // 需要监听的文件描述符,如果是-1就不监听
short events; // 需要监听的文件描述符什么事件 EPOLLIN 读事件 EPOLLOUT 写事件
short revents; // 返回监听到的事件
}

3.epoll

用得比较多,在Linux中使用得比较多

特点:

没有文件描述符1024的限制,以后每次监听都不需要在此将需要监听的文件描述符拷贝到内核,返回的是变化的文件描述符,不需要遍历树。

工作原理:

1.创建红黑树

2.将需要监听的文件描述符上树

3.监听

3.1 创建红黑树

1
2
3
4
5
#include <sys/epoll.h>
int epoll_create(int size);
参数:
size:监听的文件描述符的上限,2.6版本之后写1即可
返回值:返回树的句柄

3.2 上树、下树、修改节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
参数:
epfd:树的句柄
op:
EPOLL_CTL_ADD 上树
EPOLL_CTL_DEL 下树
EPOLL_CTL_MOD 修改树
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_data_t data; // 需要监听的文件描述符
};

3.3 监听

1
2
3
4
5
6
7
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
参数:
epfd:树的句柄
events:接收变化的节点的数组的首地址
maxevents:数组元素的个数
timeout:-1永久监听 大于0限时等待

四、TFTP

1.TFTP概述

TFTP是简单文件传送协议

最初用于引导无盘系统,被设计用来传输小文件

基于UDP协议实现,不进行用户有效性认证

数据传输模式:

octet:二进制模式

netascii:文本模式

mail:已经不再支持

2.TFTP通讯过程

1.服务器在69端口等待请求

2.服务器若批准此请求,则使用临时端口与客户端进行通信

3.每个数据包的编号都有变化(从1开始)

4.每个数据包都要得到ACK确认,如果出现超时,则需要重新发送最后的包(数据或ACK)

5.数据的长度以512Byte传输

6.小于512Byte的数据意味着传输结束

3.TFTP协议分析

注意:

以上0代表的是\0

不同的差错码对应不同的错误信息

错误码:

0 未定义,参见错误信息

1 File not found

2 Access violation

3 Disk full or allocation exceeded

4.illegal TFTP operation

5.Unknown transfer ID

6.File already exists

7.No such user

8.Unsuppored option(s) requested

4.TFTP客户端

使用TFTP协议下载server上的文件到本地

思路

1、构造请求报文,送至服务器(69号端口)

2、等待服务器回应

3、分析服务器回应

4、接收数据,直到接收到的数据包小于规定长度

代码:

五、UDP广播

1.广播的概念

广播:由一台主机向该主机所在子网内的所有主机发送数据的方式,例如192.168.3.103发送广播信息,则192.168.3.1~192.168.3.254所有主机都可以接收到数据。

广播只能用UDP或者原始IP实现,不能用TCP。

2.广播的用途

单个服务器与多个客户主机通讯时减少分组流通,以下几个协议都用到广播:

1、地址解析协议(ARP)

2、动态主机配置协议(DHCP)

3、网络时间协议(NTP)

3.广播的特点

1、处于同一子网的所有主机都必须处理数据

2、UDP数据包会沿协议栈向上一直到UDP层

3、运行音视频等较高速率工作的应用,会带来很大的负担

4、局限于局域网内使用

4.广播地址

{网络ID,主机ID}

网络ID表示由子网掩码中1覆盖的连续位

主机ID表示由子网掩码中0覆盖的连续位

**定向广播地址:**主机ID全为1

1、例如:对192.168.200.0/24,其定向广播地址为:192.168.200.255

2、通常路由器不转发该广播

**受限广播地址:**255.255.255.255

路由器从不转发该广播

未完待续…一直鸽着的