day15

本章目标

  • UDP聊天室

    未完成,先欠着,打算和Qt的一个项目做在一起。

epollsrv.cpp

epollsrv.cpp文件源代码,epoll只是服务器发生了变化而客户端不需要变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include <poll.h>
#include <iostream>
#include <vector>
#include <algorithm>
#include <sys/wait.h>
#include <sys/select.h>
#include <sys/epoll.h>
#include <fcntl.h>


#define ERR_EXIT(err) {perror(err); exit(EXIT_FAILURE);}

typedef std::vector<struct epoll_event> EventList;

typedef struct package
{
unsigned int len;
char data[1024];
}package;

void handle(int sign)
{
printf("sign = %d\n", sign);
exit(EXIT_SUCCESS);
}



//size_t read(int fd, void *buf, size_t count);
//ssize_t 无符号整数
//size_t 有符号整数
ssize_t readn(int fd, void * buf, size_t count)
{
size_t nleft = count;
ssize_t nread = 0;
char * bufp = static_cast<char *>(buf);
while(nleft > 0)
{
if((nread = read(fd, bufp, nleft)) < 0)
{
if(errno == EINTR)//如果信号被中断,仍然要继续发送数据。
continue;
//return 0;
return -1;
}
else if(nread == 0)
return count - nleft;
bufp += nread;
nleft -= nread;
}
return count;
}

//ssize_t write(int fd, const void *buf, size_t count);
//ssize_t 无符号整数
//size_t 有符号整数
ssize_t writen(int fd, void * buf, size_t count)
{

size_t nleft = count;
ssize_t nwrite;
char * bufp = static_cast<char *>(buf);
while(nleft > 0)
{
if((nwrite = write(fd, bufp, nleft)) < 0)
{
if(errno == EINTR)//如果信号被中断,仍然要继续发送数据。
continue;
return -1;
}
else if(nwrite == 0)
continue;
bufp += nwrite;
nleft -= nwrite;
}
return count;
}

ssize_t recv_peek(int sockfd, void * buf, size_t len)
{
while(1)
{ //recv读取数据但是不会把数据从缓冲区移除
int ret = recv(sockfd, buf, len, MSG_PEEK);//有数据返回,无数据阻塞,对方套接口关闭,返回零,信号中断返回-1
if(ret == -1 && errno == EINTR)//信号终止,继续进行操作
{
continue;
}
return ret;
}
}
ssize_t readline(int sockfd, void * buf, size_t maxline)
{
int ret;
int nread;
char * bufp = static_cast<char *>(buf);
int nleft = maxline;
while(1)
{
ret = recv_peek(sockfd, buf, nleft);
if(ret < 0)
return ret;
else if(ret == 0)//表示对方关闭了套接口
return ret;
nread = ret;//接收到的字节数
int i;
for(i = 0; i < nread; i++)
{
if(bufp[i] == '\n')
{
ret = readn(sockfd, bufp, i+1);//取出数据并从缓冲区移除
if(ret != i + 1)//接收数据失败,直接返回
{
printf("接收数据失败");
exit(EXIT_FAILURE);
}
return ret;
}
}
if(nread > nleft)//没有遇到'\n'把数据拿出来
{
printf("没有遇到回车换");
exit(EXIT_FAILURE);
}
nleft -= nread;
ret = readn(sockfd, bufp, nread);
if(ret != nread)//接收数据失败,直接返回
{
printf("接收数据失败");
exit(EXIT_FAILURE);
}
bufp += nread;//指针偏移,如果遇到'\n'指针已经偏移
}
return -1;
}

void activate_nonblock(int fd)
{
int ret = 0;
int flags = fcntl(fd, F_GETFL);//获取文件描述符的状态
if(fd == -1)
{
ERR_EXIT("fcntl:1");
}
flags |= O_NONBLOCK;//不阻塞模式
ret = fcntl(fd, F_SETFL, flags);//设置文件描述符的状态
if(ret == -1)
{
ERR_EXIT("fcntl:2");
}
}
void deactivate_nonblock(int fd)
{
int ret = 0;
int flags = fcntl(fd, F_GETFL);
if(flags == -1)
{
ERR_EXIT("fcntl:1");
}
flags &= ~O_NONBLOCK;//不阻塞,位操作,~按位取反
ret = fcntl(fd, F_SETFL, flags);
if(ret == -1)
{
ERR_EXIT("fcntl:1");
}
}

void signal_handle(int sign)
{
//wait(NULL);
//waitpid(-1, NULL, WNOHANG);//可能会有僵尸进程的存在,因为进程发送了SIGCHLD信号但是没有接收到,无法处理导致了僵尸进程的存在
while(waitpid(-1, NULL, WNOHANG) > 0);//如果没有子进程退出循环,避免僵尸进程
}

void signpipe(int sign)
{
printf("SIGPIPE = %d\n", sign);
}

int main()
{
//int conn = -1;
int listenfd;
struct sockaddr_in servaddr;//一个服务器的地址
signal(SIGPIPE, signpipe);//信号
//signal(SIGCHLD, SIG_IGN);//忽略信号
signal(SIGCHLD, signal_handle);//忽略信号
listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if(listenfd < 0)
{
perror("创建scoket失败\n");
exit(EXIT_FAILURE);//退出程序
}

memset(&servaddr, 0, sizeof(servaddr));//初始化地址
servaddr.sin_family = AF_INET;//地址族
servaddr.sin_port = htons(6000);//端口号,网络字节序转化为主机字节序

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示绑定本机的任意地址(返回一个主机字节序)--->和下面的两个一样作用绑定IP地址
//servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//(和上面的一样的意思)指定网络地址---> 真实的情况下表示本机的ip地址
//inet_aton("127.0.0.1", &servaddr.sin_addr);//绑定IP地址

int on = 1;
on = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if(on < 0)
{
printf("端口重复利用失败\n");
exit(EXIT_FAILURE);
}

if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
perror("绑定本地地址到套接字失败\n");
exit(EXIT_FAILURE);
}

if(listen(listenfd, SOMAXCONN) < 0)
{
perror("绑定本地地址到套接字失败\n");
exit(EXIT_FAILURE);
}

struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);//必须有初始值,否则accept会失败


std::vector<int> clients;
int epollfd;
epollfd = epoll_create1(EPOLL_CLOEXEC);//创建一个实例

struct epoll_event event;
event.data.fd = listenfd;
event.events = EPOLLIN | EPOLLET;//EPOLLIN监听,EPOLLET边沿方式触发

epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event);//添加一个套接字到epoll

EventList events(16);
struct sockaddr_in perraddr;
socklen_t perrlen;
int conn;
int count = 0;
int i;
int nready;

while(1)
{
//检测那个套接字产生的事件。 //&*:迭代器解绑定的感觉... ...
nready = epoll_wait(epollfd, &(*events.begin()), static_cast<int>(events.size()), -1);
if(nready == -1)
{
if(errno == EINTR)
continue;
ERR_EXIT("epoll_wait");
}
if(nready == 0)
continue;
if((ssize_t)nready == events.size())
if(events.size() > 4096)
events.resize(events.size() + 2048);
else
events.resize(events.size() * 2);
for(i = 0; i < nready; i++)
{
if(events[i].data.fd == listenfd)
{
perrlen = sizeof(perraddr);
conn = accept(listenfd, (struct sockaddr *)&peeraddr, &perrlen);
if(conn == -1)
ERR_EXIT("accept");
printf("peer:ip = %s, port = %d\n", inet_ntoa(perraddr.sin_addr), ntohs(perraddr.sin_port));
printf("count = %d\n", ++count);
clients.push_back(conn);
activate_nonblock(conn);//设置为非阻塞模式

//添加新的套接字到event中
event.data.fd = conn;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, conn, &event);
}
else if(events[i].events & EPOLLIN)
{
conn = events[i].data.fd;
if(conn < 0)
continue;
char recvbuf[1024] = {0};
int ret = readline(conn, recvbuf, 1024);

if(ret == -1)
ERR_EXIT("readline")
else if(ret == 0)
{
count --;
printf("client closed\n");
close(conn);
event = events[i];
epoll_ctl(epollfd, EPOLL_CTL_DEL, conn, &event);
clients.erase(std::remove(clients.begin(), clients.end(), conn), clients.end());//删除已经断开的客户端的套接字
}
fputs(recvbuf, stdout);
writen(conn, recvbuf, strlen(recvbuf));
}
}
}
return 0;
}

02 C/S模型、socket相关函数

day02

本章目标

  • TCP客户/服务器模型 —> C/S模型
  • 回射客户/服务器,echosrv.c echocli.c文件
  • socket、bind、listen、accept、connect函数

socket函数

man 2 socket 查看帮助

  • 包含头文件<sys/socket.h>

  • 功能:创建一个套接字用于通信

  • 原型:

    • int socket(int domain, int type, int protocol);
  • 参数:

    • domain :指通信协议族(protocol family)
    • type :指定socket类型。
      • 流式套接字SOCK_STREAM
      • 数据报套接字SOCK_DGRAM
      • 原始套接字SOCK_RAW
    • protocol:协议类型
  • 返回值:

    成功返回非负整数,它和文件描述符类似,我们把它称为套接字,简称套接字;

    失败返回-1

bind函数

  • 包含头文件<sys/socket.h>

  • 功能:绑定一个本地地址到套接字

  • 原型

    • ing bind(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
  • 参数

    • sockfdsockfd函数返回的套接字
    • addr:要绑定的地址
    • addrlen:地址长度
  • 返回值:

    成功返回0 ,失败返回-1

listen函数

man 2 listen SOMAXCONN 查看此宏,是第二个参数,表示队列的最大值

  • 包含头文件<sys/socket.h>
  • 功能:将套接字用于监听进入的连接
  • 原型
    • int listen(in sockfd, int backlog);
  • 参数
    • sockfdsocket函数返回的套接字
    • backlog:规定内核为此套接字排队的最大连接个数
    • 返回值:成功返回0,失败返回-1

主动套接字:主动发起连接,会调用connect()函数。

被动套接字:用来接收连接的,会调用accept()函数。

  • 一般来说,listen函数应该在调用socketbind函数之后,调用函数accept之前调用
  • 对于给定的监听套接口,内核要维护两个队列
    • 1.已由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程
    • 2.已完成连接的队列

accept函数

  • 包含头文件<sys/socket.h>
  • 功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。
  • 原型:
    • int accept(int socket, struct sockaddr * addr, socklen_t * addrlen);
  • 参数
    • sockfd:服务器套接字
    • addr:将返回对等方的套接字地址
    • addrlen:返回对等方的套接字地址长度。是一个输入输出的参数,所以一定要初始化。
  • 返回值:成功返回i非负整数,失败返回-1.

connect函数

  • 包含头文件<sys/socket.h>
  • 功能:建立一个连接至addr所指定的套接字
  • 原型
    • int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
  • 参数
    • sockfd:未连接套接字
    • addr:要连接的套接字地址
    • adrlen:第二个参数addr长度
    • 返回值:成功返回0,失败返回-1

01 socket 字节序&地址转换函数&套接字类型

day01

本章目标

  • 什么是socket?
  • IPv4套接口地址结构
  • 网络字节序
  • 地址转换函数
  • 套接字类型

什么是socket?

socket可以看成是用户进程与内核网络协议栈的编程接口;

socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程通信。

套接口通信是全双工通信。

socket可以异构通信,通信双方的软件、硬件可以不同。

IPv4套接口地址结构

IPv4套接口地址结构通常也称为”网际套接字地址结构”,它以”sockaddr_in”命名,定义在头文件<netinet/in.h>中****

man 7 ip查看sockaddr_in结构体,sockaddr_in仅仅应用于IPv4地址结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct sockaddr_in

{

>​ uint8_t sin_len;

>​ sa_family_t sin_family;

>​ in_port_t sin_port;

>​ struct in_addr sin_addr;

>​ char sin_zero[8];

>}

sin_len:整个sockaddr_in结构体的长度,在4.3BSD-Reno版本之前的第一个成员是sin_family

sin_family:指定该地址家族,在这里必须设为AF_INET

sin_port:端口;

sin_addr:IPv4的地址;

sin_zero:暂不使用,一般将其设置为0。

通用地址结构

通用地址结构用来指定套接字关联的地址,sockaddr可以应用于所有的协议的套接口

1
2
3
4
5
6
7
8
9
10
11
>struct sockaddr

>{

>​ uint8_t sin_len;

>​ sa_family_t sin_family;

>​ char sa_data[14];

>}

sin_len:整个sockaddr结构体的长度;

sin_family:指定该地址家族;

sa_data:由sin_family决定它的形式。

网络字节序

https://www.cnblogs.com/gremount/p/8830707.html

  • 字节序

    • 大端字节序(Big Endian)

      最高有效位(MSB: Most Significant Bit)存储于最低内存地址处,最低有效位(LSB: Lowest Significant Bit)存储于最高内存地址处。

    • 小端字节序(Little Endian)

      最高有效位(MSB:Most Significant Bit)存储于最高内存地址处,最低有效位(LSB: Lowest Significant Bit)存储于最低内存地址处。

      假设有字节序0x12345678

内存地址 低地址 –> –> 高地址
大端字节序(0x) 12 34 56 78
小端字节序(0x) 78 56 34 12
  • 主机字节序

    不同的主机有不同的字节序,如x86为小端字节序,Motorola 6800为大端字节序,ARM字节序是可配置的。

  • 网络字节序

    网络字节序规定为大端字节序

为什么有字节序

计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

字节序转换函数

man 3 byteorder 查询字节序转换函数

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

uint32_t htonl(uint32_t hostlong);//host to network long 四个字节

uint16_t htons(uint16_t hostshort);//host to network short 两个字节

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);

注:在上述的函数中,h 代表 host; n 代表 network; s 代表 short; l 代表 long

地址转换函数

man 3 inet 查询地址转换函数

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

int inet_aton(const char *cp, struct in_addr *inp);

in_addr_t inet_network(const char *cp);//点分十进制的地址转化为32位无符号长整形

char *inet_ntoa(struct in_addr in);//32位无符号长整形转化位点分十进制

typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};

套接字类型

  • 流式套接字(SOCK_STREAM) —> TCP/IP协议

    提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。

  • 数据报式套接字(SOCK_DGRAM)

    提供无连接服务,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。

  • 原始套接字(SOCK_RAW)

    跨越传输层,直接对IP层进行数据封装的套接字,通过原始套接字,将应用层的数据封装成IP 层能够认识的套接字。

04 TCP粘包问题原因及解决

day04

本章目标

  • 流协议与粘包:**TCP是字节流协议会导致粘包问题的出现**
  • 粘包产生的原因
  • 粘包处理方案
  • readnwriten
  • 回射客户/服务器

TCP是字节流传输,信息无边界。

UDP是消息传输,传输的是报文,是具有边界的。

粘包解决方案

  • 本质上是要在应用层维护消息与消息的边界
    • 定包长
    • 包尾加\r\n(ftp协议处理方法)
    • 包头加上包体长度
    • 更复杂的应用层协议

readn 和writen函数

自定义通信协议,即为自定义数据包结构。

  • readn 接收确切数目的读操作
  • write发送确切数目的写操作

03 地址重复利用、处理多客户端的连接

day03

本章目标

  • REUSEADDR,地址重复利用
  • 处理多客户端连接(process-per-conection)
  • 点对点聊天程序实现

REUSEADDR

  • 服务器端尽可能使用REUSEADDR
  • 在绑定之前尽可能调用setsockopt来设置REUSEADDR套接字选项。
  • 使用REUSEADDR选项可以使得不必等待TIME——WAIT状态消失就可以重启服务器。

process-per-connection

  • 一个连接一个进程来处理并发,多进程处理多客户端的连接。
  • 每一个客户端都有一个独立的进程,所以通信不会出现串线。

点对点聊天程序实现

  • 用多进程方式实现点对点聊天

getppid()获取父进程的pid

echosrv.c源文件

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

void handle(int sign)
{
printf("sign = %d\n", sign);
exit(EXIT_SUCCESS);
}

int main()
{
int listenfd;
struct sockaddr_in servaddr;//一个服务器的地址
//listenfd = socket(AF_INET, SOCK_STREAM, 0);
////和下面的一样,第三个参数为0表示自动获取协议。

listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if(listenfd < 0)
{
perror("创建scoket失败\n");
exit(EXIT_FAILURE);//退出程序
}

memset(&servaddr, 0, sizeof(servaddr));//初始化地址
servaddr.sin_family = AF_INET;//地址族
servaddr.sin_port = htons(5188);//端口号,网络字节序转化为主机字节序

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//表示绑定本机的任意地址(返回一个主机字节序)--->和下面的两个一样作用绑定IP地址
//servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//(和上面的一样的意思)指定网络地址---> 真实的情况下表示本机的ip地址
//inet_aton("127.0.0.1", &servaddr.sin_addr);//绑定IP地址

int on = 1;
on = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if(on < 0)
{
printf("端口重复利用失败\n");
exit(EXIT_FAILURE);
}

if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
perror("绑定本地地址到套接字失败\n");
exit(EXIT_FAILURE);
}

if(listen(listenfd, SOMAXCONN) < 0)
{
perror("绑定本地地址到套接字失败\n");
exit(EXIT_FAILURE);
}

struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);//必须有初始值,否则accept会失败

int conn = -1;
if((conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen) )< 0)
{
perror("接收连接失败\n");
exit(EXIT_FAILURE);
}
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("创建进程失败\n");
exit(EXIT_FAILURE);
}
else if(pid == 0)//子进程发送数据
{

signal(SIGUSR1, handle);
char sendbuf[1024] = {0};
while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
write(conn, sendbuf, strlen(sendbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
}
else//父进程接收数据
{
char recvbuf[1024] = {0};
while(1)
{
fflush(stdin);
memset(recvbuf, 0, sizeof(recvbuf));
int ret = read(conn, recvbuf, sizeof(recvbuf));

if(ret == -1)
{
printf("获取客户端的数据失败\n");
break;
//exit(EXIT_FAILURE);
}
else if(ret == 0)
{
printf("客户端关闭连接\n");
break;
//exit(EXIT_SUCCESS);
}
printf("服务器接收到的数据:");
fputs(recvbuf, stdout);
//write(conn, recvbuf, ret);
}
close(conn);
printf("parent die\n");
kill(pid, SIGUSR1);
exit(EXIT_SUCCESS);
}


close(listenfd);
close(conn);
return 0;
}

echocli.c源文件

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

void handle(int sign)
{
printf("sign = %d\n", sign);
exit(EXIT_SUCCESS);
}

int main()
{
int sock;
struct sockaddr_in servaddr;//一个服务器的地址
//sock = socket(AF_INET, SOCK_STREAM, 0);
////和下面的一样,第三个参数为0表示自动获取协议。

sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sock < 0)
{
perror("创建scoket失败\n");
exit(EXIT_FAILURE);//退出程序
}
memset(&servaddr, 0, sizeof(servaddr));//初始化地址
servaddr.sin_family = AF_INET;//地址族
servaddr.sin_port = htons(5188);//端口号,网络字节序转化为主机字节序
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");//指定服务端的地址

int conn = -1;
if((conn = connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0))
{
printf("连接服务器失败\n");
exit(EXIT_FAILURE);
}

pid_t pid;
pid = fork();
if(pid < 0)
{
printf("创建进程失败\n");
exit(EXIT_FAILURE);
}
else if(pid > 0)//父进程接收数据
{
char recvbuf[1024] = {0};
while(1)
{
fflush(stdin);
memset(recvbuf, 0, sizeof(recvbuf));
int ret = read(sock, recvbuf, sizeof(recvbuf));
if(ret == -1)
{
printf("客户端接收从服务器的数据失败\n");
//:close(sock);
break;
//exit(EXIT_FAILURE);
}
else if(ret == 0)
{
printf("服务器断开了连接\n");
//close(sock);
break;
//exit(EXIT_SUCCESS);
}
printf("客户端接收到的数据:");
fputs(recvbuf, stdout);
}
close(sock);
printf("parent die\n");
kill(pid, SIGUSR1);//getppid(),获取父进程的pid
exit(EXIT_SUCCESS);
}
else//子进程发送数据
{
char sendbuf[1024] = {0};
signal(SIGUSR1, handle);
while(fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
{
write(sock, sendbuf, strlen(sendbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
}
close(sock);
return 0;
}

05 readline实现、get*name系列函数

day05

本章目标

  • readwriterecvsend
  • readline实现
  • readline实现回射客户端/服务器
  • getsocknamegetpeername
  • gethostnamegethostbynamegethostbyadr

read、write与recv、send函数

man recv

recv函数只能用于套接口的I/O· ,read函数可以用于任何的I/O

recv函数增加了可选参数flgs,flags取值为MSG_OOB :接收紧急指针数据。MSG_PEEK:接收数据但是不把缓冲区的数据清除。

1
2
3
4
5
6
#include <sys/types.h>
#include <sys/socket.h>

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

##flags标志:

1
2
3
4
5
6
7
8
9
10
11
MSG_OOB
This flag requests receipt of out-of-band data that would not be
received in the normal data stream. Some protocols place expe‐
dited data at the head of the normal data queue, and thus this
flag cannot be used with such protocols.

MSG_PEEK
This flag causes the receive operation to return data from the
beginning of the receive queue without removing that data from
the queue. Thus, a subsequent receive call will return the same
data.

readline实现

readline:是为了解决粘包问题。

getsockname、getpeername

getsockname:获取套接口本地的地址

getpeername:获取远程套接口的地址

1
2
3
4
5
6
7
8
#include <sys/socket.h>

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

struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
1
2
3
4
5
6
7
8
#include <sys/socket.h>

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

struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}

gethostname、gethostbyname和gethostbyaddr

gethostname:获取主机名字

gethostbyname:通过主机名获取所有的IP地址

1
2
3
#include <unistd.h>

int gethostname(char *name, size_t len);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <netdb.h>
extern int h_errno;

struct hostent *gethostbyname(const char *name);

#include <sys/socket.h> /* for AF_INET */
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);

struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses *///IP地址列表
}
#define h_addr h_addr_list[0] /* for backward compatibility */

注:struct hostent的成员h_addr_list虽然是char **但是是需要用inet_ntoa转化一下才能输出IP地址。

06 TCP回射C/S&僵尸进程&SIGCHLD信号

day06

参考链接:

https://www.cnblogs.com/yuxingfirst/p/3165407.html

本章目标

  • TCP回射客户/服务器

  • TCP是个流协议

  • 僵尸进程与SIGCHLD信号

TCP是一个流协议

  • TCP基于字节流传输的,只维护发送出去多少,确认收到了多少,没有维护消息与消息之间的边界,因而可能导致粘包问题,所以需要自己维护。
  • 粘包问题解决方法是在应用层维护消息边界
    • 粘包问题
      • 定长包
      • 使用\n作为区分消息和消息的边界

僵尸进程的避免

僵尸进程:子进程死后,父进程未给子进程收尸,即父进程未回收子进程的资源

孤儿进程:父进程先于子进程死亡,子进程没有了父进程,成为了孤儿进程,而linux操作系统有一个孤儿院,进程ID为1的进程,会成为孤儿进程的父进程,使孤儿进程的资源可以让父进程回收

1
2
3
4
5
[pip@localhost code]$ ps -ef | grep echosrv
pip 4213 4145 0 17:40 pts/1 00:00:00 vim echosrv.c
pip 5237 4240 0 18:11 pts/2 00:00:00 ./echosrv
pip 5240 5237 0 18:11 pts/2 00:00:00 [echosrv] <defunct>
pip 5245 4289 0 18:11 pts/3 00:00:00 grep --color=auto echosrv

第三行即为僵尸进程。

  • SIGCHLD信号
1
2
singal(SIGCHLD, SIG_IGN);//忽略SIGCHLD信号
signal(SIGCHLD, handle_signld);//捕捉SIGCHLD信号,并对其进行处理,handle_signld是一个函数指针,用来处理SIGCHLD信号,返回值为零,参数为int型
  • wait()函数
1
2
3
4
5
6
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);

pid_t wait(int *status);获取子进程的状态,传入NULL即为不关心子进程状态。

  • 第一种处理结果

    1
    2
    3
    pip       4213  4145  0 17:40 pts/1    00:00:01 vim echosrv.c
    pip 5378 4240 0 18:13 pts/2 00:00:00 ./echosrv
    pip 5386 4289 0 18:13 pts/3 00:00:00 grep --color=auto echosrv
  • 第二种处理结果

    1
    2
    3
    pip       4213  4145  0 17:40 pts/1    00:00:01 vim echosrv.c
    pip 5446 4240 0 18:15 pts/2 00:00:00 ./echosrv
    pip 5454 4289 0 18:15 pts/3 00:00:00 grep --color=auto echosrv

    由上两种处理结果发现僵尸进程都不存在了,则使用``SIGCHLD`可以避免僵尸进程的出现。

    如果有多个子进程那么就会出现问题,wait只会处理第一个返回的子进程,然后返回,如果需要处理多个子进程需要使用waitpid等待指定的只进程返回。

    点击查看相关博客

多个子进程的僵尸进程处理

如果有多个子进程的话,上述两种方式都可能会产生僵尸进程,相信如果是第一种情况对于多个子进程处理是肯定有僵尸进程的,因为wait只等待一个子进程就退出了,而虽然waitpid可以等待多个子进程,但是如果子进程同时发送SIGCHLD信号,那么父进程可能不能接收到子进程的SIGCHLD信号,导致不能对子进程的资源进行回收。使子进程成为了僵尸进程。

  • 更加好的回收的子进程资源的方式出现了

    1
    2
    3
    4
    5
    6
    void signal_handle(int sign)
    {
    //wait(NULL);//只接受一个子进程的的SIGCHLD然后直接返回
    //waitpid(-1, NULL, WNOHANG);//可能会有僵尸进程的存在,因为进程发送了SIGCHLD信号但是没有接收到,无法处理导致了僵尸进程的存在
    while(waitpid(-1, NULL, WNOHANG) > 0);//如果没有子进程退出循环,彻底避免僵尸进程
    }

    RETURN VALUE//返回值
    wait(): on success, returns the process ID of the terminated child; on error, -1 is returned.
    waitpid(): on success, returns the process ID of the child whose state has changed; if WNOHANG was specified and one or more child(ren) specified by pid exist, but have not yet changed state, then 0 is returned. On error, -1 is returned.

07 TCP的11种状态、SIGPIPE信号

day07

本章目标

  • TCP 11种状态
  • 连接建立三次握手、连接终止四次握手
  • TIME_WAITSO_REUSEADDR
  • SIGPIPE

ACK、SYN和FIN

参考文档:
https://zhidao.baidu.com/question/27114298.html

ACK:是一种确认应答,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。
SYN:攻击属于DOS攻击的一种,它利用TCP协议缺陷,通过发送大量的半连接请求,耗费CPU和内存资源。是最常见又最容易被利用的一种攻击手法。
FIN:是用来扫描保留的端口,发送一个FIN包(或者是任何没有ACK或SYN标记的包)到目标的一个开放的端口,然后等待回应。许多系统会返回一个复位标记。

TCP 11种状态

tcp的11种状态

貌似有点不对啊,不是说是11种状态吗???怎么只有10种,莫非打错字了???NO!!!NO!!!NO!!!

第十一种状态为:CLOSING

关闭请求由双方都可以主动的发起,图中为客户端发起的中断连接,也可以是服务器发起中断连接。

TIME_WAIT:保留两倍的MSL时间。

参考:MSL,即 Maximum Segment Lifetime,一个数据分片(报文)在网络中能够生存的最长时间,在RFC793中定义MSL通常为2分钟,即超过两分钟即认为这个报文已经在网络中被丢弃了。对于一个TCP连接,在双方进入TIME_WAIT后,通常会等待2倍MSL时间后,再关闭掉连接,作用是为了防止由于FIN报文丢包,对端重发导致与后续的TCP连接请求产生顺序混乱,具体原理这里就不详细解释了,

参考链接:

https://blog.csdn.net/qwertyupoiuytr/article/details/71436967

CLOSING状态

如果客户端和服务器双方同时关闭close时,会产生此状态,建议参考连接。

参考文档:

https://blog.csdn.net/dog250/article/details/52070680

验证查看ESTABLISHED、FIN_WAIT2状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[pip@localhost code]$ netstat -an | grep tcp | grep 6000
tcp 0 0 0.0.0.0:6000 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:34004 127.0.0.1:6000 ESTABLISHED
tcp 0 0 127.0.0.1:6000 127.0.0.1:34004 ESTABLISHED

[pip@localhost code]$ ps -aux | grep echosrv
pip 5332 0.0 0.1 149292 5356 pts/2 S+ 11:25 0:00 vim echosrv.c
pip 5626 0.0 0.0 4176 348 pts/3 S+ 11:26 0:00 ./echosrv
pip 5629 0.0 0.0 4176 92 pts/3 S+ 11:26 0:00 ./echosrv
pip 5792 0.0 0.0 112664 968 pts/5 S+ 11:31 0:00 grep --color=auto echosrv

[pip@localhost code]$ kill -9 5626
[pip@localhost code]$ netstat -an | grep tcp | grep 6000
tcp 1 0 127.0.0.1:34004 127.0.0.1:6000 CLOSE_WAIT
tcp 0 0 127.0.0.1:6000 127.0.0.1:34004 FIN_WAIT2
[pip@localhost code]$

SIGPIPE信号

  • 往一个已经接收FIN的套接字种写是允许的,接收到FIN仅仅代表对方不再发送数据

  • 在收到RST段之后,如果再调用write就会产生SIGNPIPE信号,对于这个信号的处理,我们通常忽略即可

    signal(SIGPIPE, SIG_IGN);

参考文档:

https://blog.csdn.net/aa838260772/article/details/39854323