Linux网络编程——黑马程序员笔记
Posted 行稳方能走远
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux网络编程——黑马程序员笔记相关的知识,希望对你有一定的参考价值。
01P-复习-Linux网络编程
02P-信号量生产者复习
03P-协议
协议:
一组规则。
04P-7层模型和4层模型及代表协议
分层模型结构:
OSI七层模型: 物、数、网、传、会、表、应
TCP/IP 4层模型:网(链路层/网络接口层)、网、传、应
应用层:http、ftp、nfs、ssh、telnet。。。
传输层:TCP、UDP
网络层:IP、ICMP、IGMP
链路层:以太网帧协议、ARP
05P-网络传输数据封装流程
网络传输流程:
数据没有封装之前,是不能在网络中传递。
数据-》应用层-》传输层-》网络层-》链路层 --- 网络环境
06P-以太网帧和ARP请求
以太网帧协议:
ARP协议:根据 Ip 地址获取 mac 地址。
以太网帧协议:根据mac地址,完成数据包传输。
07P-IP协议
IP协议:
版本: IPv4、IPv6 -- 4位
TTL: time to live 。 设置数据包在路由节点中的跳转上限。每经过一个路由节点,该值-1, 减为0的路由,有义务将该数据包丢弃
源IP: 32位。--- 4字节 192.168.1.108 --- 点分十进制 IP地址(string) --- 二进制
目的IP:32位。--- 4字节
08P-端口号和UDP协议
UDP:
16位:源端口号。 2^16 = 65536
16位:目的端口号。
IP地址:可以在网络环境中,唯一标识一台主机。
端口号:可以网络的一台主机上,唯一标识一个进程。
ip地址+端口号:可以在网络环境中,唯一标识一个进程。
09P-TCP协议
TCP协议:
16位:源端口号。 2^16 = 65536
16位:目的端口号。
32序号;
32确认序号。
6个标志位。
16位窗口大小。 2^16 = 65536
10P-BS和CS模型对比
c/s模型:
client-server
b/s模型:
browser-server
C/S B/S
优点: 缓存大量数据、协议选择灵活 安全性、跨平台、开发工作量较小
速度快
缺点: 安全性、跨平台、开发工作量较大 不能缓存大量数据、严格遵守 http
11P-套接字
网络套接字: socket
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。)
在通信过程中, 套接字一定是成对出现的。
12P-回顾
13P-网络字节序
网络字节序:
小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678
大端法:(网络存储) 高位存低地址。地位存高地址。
htonl --> 本地--》网络 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序
htons --> 本地--》网络 (port)
ntohl --> 网络--》 本地(IP)
ntohs --> 网络--》 本地(Port)
14P-IP地址转换函数
IP地址转换函数:
int inet_pton(int af, const char *src, void *dst); 本地字节序(string IP) ---> 网络字节序
af:AF_INET、AF_INET6
src:传入,IP地址(点分十进制)
dst:传出,转换后的 网络字节序的 IP地址。
返回值:
成功: 1
异常: 0, 说明src指向的不是一个有效的ip地址。
失败:-1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 网络字节序 ---> 本地字节序(string IP)
af:AF_INET、AF_INET6
src: 网络字节序IP地址
dst:本地字节序(string IP)
size: dst 的大小。
返回值: 成功:dst。
失败:NULL
15P-sockaddr地址结构
sockaddr地址结构: IP + port --> 在网络环境中唯一标识一个进程。
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6 man 7 ip
addr.sin_port = htons(9527);
int dst;
inet_pton(AF_INET, "192.157.22.45", (void *)&dst);
addr.sin_addr.s_addr = dst;
【*】addr.sin_addr.s_addr = htonl(INADDR_ANY); 取出系统中有效的任意IP地址。二进制类型。
bind(fd, (struct sockaddr *)&addr, size);
16P-socket模型创建流程分析
17P-socket和bind
socket函数:
#include <sys/socket.h>
int socket(int domain, int type, int protocol); 创建一个 套接字
domain:AF_INET、AF_INET6、AF_UNIX
type:SOCK_STREAM、SOCK_DGRAM
protocol: 0
返回值:
成功: 新套接字所对应文件描述符
失败: -1 errno
#include <arpa/inet.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 给socket绑定一个 地址结构 (IP+port)
sockfd: socket 函数返回值
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr: 传入参数(struct sockaddr *)&addr
addrlen: sizeof(addr) 地址结构的大小。
返回值:
成功:0
失败:-1 errno
18P-listen和accept
int listen(int sockfd, int backlog); 设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
sockfd: socket 函数返回值
backlog:上限数值。最大值 128.
返回值:
成功:0
失败:-1 errno
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。
sockfd: socket 函数返回值
addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)
socklen_t clit_addr_len = sizeof(addr);
addrlen:传入传出。 &clit_addr_len
入:addr的大小。 出:客户端addr实际大小。
返回值:
成功:能与客户端进行数据通信的 socket 对应的文件描述。
失败: -1 , errno
19P-connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用现有的 socket 与服务器建立连接
sockfd: socket 函数返回值
struct sockaddr_in srv_addr; // 服务器地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致。
inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);
addr:传入参数。服务器的地址结构
addrlen:服务器的地址结构的大小
返回值:
成功:0
失败:-1 errno
如果不使用bind绑定客户端地址结构, 采用"隐式绑定".
20P-CS模型的TCP通信分析
TCP通信流程分析:
server:
1. socket() 创建socket
2. bind() 绑定服务器地址结构
3. listen() 设置监听上限
4. accept() 阻塞监听客户端连接
5. read(fd) 读socket获取客户端数据
6. 小--大写 toupper()
7. write(fd)
8. close();
client:
1. socket() 创建socket
2. connect(); 与服务器建立连接
3. write() 写数据到 socket
4. read() 读转换后的数据。
5. 显示读取结果
6. close()
21P-server的实现
代码如下:
- #include <stdio.h>
- #include <ctype.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- #define SERV_PORT 9527
- void sys_err(const char *str)
-
perror(str);
-
exit(1);
- int main(int argc, char *argv[])
-
int lfd = 0, cfd = 0;
-
int ret, i;
-
char buf[BUFSIZ], client_IP[1024];
-
struct sockaddr_in serv_addr, clit_addr; // 定义服务器地址结构 和 客户端地址结构
-
socklen_t clit_addr_len; // 客户端地址结构大小
-
serv_addr.sin_family = AF_INET; // IPv4
-
serv_addr.sin_port = htons(SERV_PORT); // 转为网络字节序的 端口号
-
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 获取本机任意有效IP
-
lfd = socket(AF_INET, SOCK_STREAM, 0); //创建一个 socket
-
if (lfd == -1)
-
sys_err("socket error");
-
-
bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));//给服务器socket绑定地址结构(IP+port)
-
listen(lfd, 128); // 设置监听上限
-
clit_addr_len = sizeof(clit_addr); // 获取客户端地址结构大小
-
cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len); // 阻塞等待客户端连接请求
-
if (cfd == -1)
-
sys_err("accept error");
-
printf("client ip:%s port:%d\\n",
-
inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)),
-
ntohs(clit_addr.sin_port)); // 根据accept传出参数,获取客户端 ip 和 port
-
while (1)
-
ret = read(cfd, buf, sizeof(buf)); // 读客户端数据
-
write(STDOUT_FILENO, buf, ret); // 写到屏幕查看
-
for (i = 0; i < ret; i++) // 小写 -- 大写
-
buf[i] = toupper(buf[i]);
-
write(cfd, buf, ret); // 将大写,写回给客户端。
-
-
close(lfd);
-
close(cfd);
-
return 0;
编译测试,结果如下:
22P-获取客户端地址结构
cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len);
accept函数中的clit_addr传出的就是客户端地址结构,IP+port
于是,在代码中增加此段代码,可获取客户端信息:
printf(“client ip:%s port:%d\\n”,
inet_ntop(AF_INET,&clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)),
ntohs(clit_addr.sin_port));
上一节代码中已经有这段代码,这里就不再跑一遍了。
23P-client的实现
- #include <stdio.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- #define SERV_PORT 9527
- void sys_err(const char *str)
-
perror(str);
-
exit(1);
- int main(int argc, char *argv[])
-
int cfd;
-
int conter = 10;
-
char buf[BUFSIZ];
-
struct sockaddr_in serv_addr; //服务器地址结构
-
serv_addr.sin_family = AF_INET;
-
serv_addr.sin_port = htons(SERV_PORT);
-
//inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
-
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
-
cfd = socket(AF_INET, SOCK_STREAM, 0);
-
if (cfd == -1)
-
sys_err("socket error");
-
int ret = connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
-
if (ret != 0)
-
sys_err("connect err");
-
while (--conter)
-
write(cfd, "hello\\n", 6);
-
ret = read(cfd, buf, sizeof(buf));
-
write(STDOUT_FILENO, buf, ret);
-
sleep(1);
-
-
close(cfd);
-
return 0;
编译运行,结果如下:
这里遇到过一个问题,如果之前运行server,用Ctrl+z终止进程,ps aux列表里会有服务器进程残留,这个会影响当前服务器。解决方法是kill掉这些服务器进程。不然端口被占用,当前运行的服务器进程接收不到东西,没有回显。
24P-总结
协议:
一组规则。
分层模型结构:
OSI七层模型: 物、数、网、传、会、表、应
TCP/IP 4层模型:网(链路层/网络接口层)、网、传、应
应用层:http、ftp、nfs、ssh、telnet。。。
传输层:TCP、UDP
网络层:IP、ICMP、IGMP
链路层:以太网帧协议、ARP
c/s模型:
client-server
b/s模型:
browser-server
C/S B/S
优点: 缓存大量数据、协议选择灵活 安全性、跨平台、开发工作量较小
速度快
缺点: 安全性、跨平台、开发工作量较大 不能缓存大量数据、严格遵守 http
网络传输流程:
数据没有封装之前,是不能在网络中传递。
数据-》应用层-》传输层-》网络层-》链路层 --- 网络环境
以太网帧协议:
ARP协议:根据 Ip 地址获取 mac 地址。
以太网帧协议:根据mac地址,完成数据包传输。
IP协议:
版本: IPv4、IPv6 -- 4位
TTL: time to live 。 设置数据包在路由节点中的跳转上限。每经过一个路由节点,该值-1, 减为0的路由,有义务将该数据包丢弃
源IP: 32位。--- 4字节 192.168.1.108 --- 点分十进制 IP地址(string) --- 二进制
目的IP:32位。--- 4字节
IP地址:可以在网络环境中,唯一标识一台主机。
端口号:可以网络的一台主机上,唯一标识一个进程。
ip地址+端口号:可以在网络环境中,唯一标识一个进程。
UDP:
16位:源端口号。 2^16 = 65536
16位:目的端口号。
TCP协议:
16位:源端口号。 2^16 = 65536
16位:目的端口号。
32序号;
32确认序号。
6个标志位。
16位窗口大小。 2^16 = 65536
网络套接字: socket
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。)
在通信过程中, 套接字一定是成对出现的。
网络字节序:
小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678
大端法:(网络存储) 高位存低地址。地位存高地址。
htonl --> 本地--》网络 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序
htons --> 本地--》网络 (port)
ntohl --> 网络--》 本地(IP)
ntohs --> 网络--》 本地(Port)
IP地址转换函数:
int inet_pton(int af, const char *src, void *dst); 本地字节序(string IP) ---> 网络字节序
af:AF_INET、AF_INET6
src:传入,IP地址(点分十进制)
dst:传出,转换后的 网络字节序的 IP地址。
返回值:
成功: 1
异常: 0, 说明src指向的不是一个有效的ip地址。
失败:-1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 网络字节序 ---> 本地字节序(string IP)
af:AF_INET、AF_INET6
src: 网络字节序IP地址
dst:本地字节序(string IP)
size: dst 的大小。
返回值: 成功:dst。
失败:NULL
sockaddr地址结构: IP + port --> 在网络环境中唯一标识一个进程。
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6 man 7 ip
addr.sin_port = htons(9527);
int dst;
inet_pton(AF_INET, "192.157.22.45", (void *)&dst);
addr.sin_addr.s_addr = dst;
【*】addr.sin_addr.s_addr = htonl(INADDR_ANY); 取出系统中有效的任意IP地址。二进制类型。
bind(fd, (struct sockaddr *)&addr, size);
socket函数:
#include <sys/socket.h>
int socket(int domain, int type, int protocol); 创建一个 套接字
domain:AF_INET、AF_INET6、AF_UNIX
type:SOCK_STREAM、SOCK_DGRAM
protocol: 0
返回值:
成功: 新套接字所对应文件描述符
失败: -1 errno
#include <arpa/inet.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 给socket绑定一个 地址结构 (IP+port)
sockfd: socket 函数返回值
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr: 传入参数(struct sockaddr *)&addr
addrlen: sizeof(addr) 地址结构的大小。
返回值:
成功:0
失败:-1 errno
int listen(int sockfd, int backlog); 设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
sockfd: socket 函数返回值
backlog:上限数值。最大值 128.
返回值:
成功:0
失败:-1 errno
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。
sockfd: socket 函数返回值
addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)
socklen_t clit_addr_len = sizeof(addr);
addrlen:传入传出。 &clit_addr_len
入:addr的大小。 出:客户端addr实际大小。
返回值:
成功:能与客户端进行数据通信的 socket 对应的文件描述。
失败: -1 , errno
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用现有的 socket 与服务器建立连接
sockfd: socket 函数返回值
struct sockaddr_in srv_addr; // 服务器地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致。
inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);
addr:传入参数。服务器的地址结构
addrlen:服务器的地址结构的大小
返回值:
成功:0
失败:-1 errno
如果不使用bind绑定客户端地址结构, 采用"隐式绑定".
TCP通信流程分析:
server:
1. socket() 创建socket
2. bind() 绑定服务器地址结构
3. listen() 设置监听上限
4. accept() 阻塞监听客户端连接
5. read(fd) 读socket获取客户端数据
6. 小--大写 toupper()
7. write(fd)
8. close();
client:
1. socket() 创建socket
2. connect(); 与服务器建立连接
3. write() 写数据到 socket
4. read() 读转换后的数据。
5. 显示读取结果
6. close()
25P-复习
26P-三次握手建立连接
27P-数据通信
并不是一次发送,一次应答。也可以批量应答
28P-四次握手关闭连接
29P-半关闭补充说明
这里其实就是想说明,完成两次挥手后,不是说两端的连接断开了,主动端关闭了写缓冲区,不能再向对端发送数据,被动端关闭了读缓冲区,不能再从对端读取数据。然而主动端还是能够读取对端发来的数据。
30P-滑动窗口和TCP数据包格式
滑动窗口:
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。
31P-通信时序与代码对应关系
32P-TCP通信时序总结
三次握手:
主动发起连接请求端,发送 SYN 标志位,请求建立连接。 携带序号号、数据字节数(0)、滑动窗口大小。
被动接受连接请求端,发送 ACK 标志位,同时携带 SYN 请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小。
主动发起连接请求端,发送 ACK 标志位,应答服务器连接请求。携带确认序号。
四次挥手:
主动关闭连接请求端, 发送 FIN 标志位。
被动关闭连接请求端, 应答 ACK 标志位。 ----- 半关闭完成。
被动关闭连接请求端, 发送 FIN 标志位。
主动关闭连接请求端, 应答 ACK 标志位。 ----- 连接全部关闭
滑动窗口:
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。
33P-错误处理函数的封装思路
wrap.h文件如下,就是包裹函数的声明
- #ifndef _WRAP_H
- #define _WRAP_H
- void perr_exit(const char *s);
- int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
- int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
- int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
- int Listen(int fd, int backlog);
- int Socket(int family, int type, int protocol);
- ssize_t Read(int fd, void *ptr, size_t nbytes);
- ssize_t Write(int fd, const void *ptr, size_t nbytes);
- int Close(int fd);
- ssize_t Readn(int fd, void *vptr, size_t n);
- ssize_t Writen(int fd, const void *vptr, size_t n);
- ssize_t my_read(int fd, char *ptr);
- ssize_t Readline(int fd, void *vptr, size_t maxlen);
- #endif
wrap.c随便取一部分,如下,就是包裹函数的代码:
- #include <stdlib.h>
- #include <stdio.h>
- #include <unistd.h>
- #include <errno.h>
- #include <sys/socket.h>
- void perr_exit(const char *s)
-
perror(s);
-
exit(-1);
- int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
-
int n;
- again:
-
if ((n = accept(fd, sa, salenptr)) < 0)
-
if ((errno == ECONNABORTED) || (errno == EINTR))
-
goto again;
-
else
-
perr_exit("accept error");
-
-
return n;
- int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
-
int n;
-
if ((n = bind(fd, sa, salen)) < 0)
-
perr_exit("bind error");
-
return n;
这里原函数和包裹函数的函数名差异只有首字母大写,这是因为man page对字母大小写不敏感,同名的包裹函数一样可以跳转至man page
34P-错误处理函数封装
就是重新包裹需要检查返回值的函数,让代码不那么肥胖。
35P-封装思想总结和readn、readline封装思想说明
错误处理函数:
封装目的:
在 server.c 编程过程中突出逻辑,将出错处理与逻辑分开,可以直接跳转man手册。
【wrap.c】 【wrap.h】
存放网络通信相关常用 自定义函数 存放 网络通信相关常用 自定义函数原型(声明)。
命名方式:系统调用函数首字符大写, 方便查看man手册
如:Listen()、Accept();
函数功能:调用系统调用函数,处理出错场景。
在 server.c 和 client.c 中调用 自定义函数
联合编译 server.c 和 wrap.c 生成 server
client.c 和 wrap.c 生成 client
readn:
读 N 个字节
readline:
读一行
36P-中午复习
三次握手:
主动发起连接请求端,发送 SYN 标志位,请求建立连接。 携带序号号、数据字节数(0)、滑动窗口大小。
被动接受连接请求端,发送 ACK 标志位,同时携带 SYN 请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小。
主动发起连接请求端,发送 ACK 标志位,应答服务器连接请求。携带确认序号。
四次挥手:
主动关闭连接请求端, 发送 FIN 标志位。
被动关闭连接请求端, 应答 ACK 标志位。 ----- 半关闭完成。
被动关闭连接请求端, 发送 FIN 标志位。
主动关闭连接请求端, 应答 ACK 标志位。 ----- 连接全部关闭
滑动窗口:
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。
错误处理函数:
封装目的:
在 server.c 编程过程中突出逻辑,将出错处理与逻辑分开,可以直接跳转man手册。
【wrap.c】 【wrap.h】
存放网络通信相关常用 自定义函数 存放 网络通信相关常用 自定义函数原型(声明)。
命名方式:系统调用函数首字符大写, 方便查看man手册
如:Listen()、Accept();
函数功能:调用系统调用函数,处理出错场景。
在 server.c 和 client.c 中调用 自定义函数
联合编译 server.c 和 wrap.c 生成 server
client.c 和 wrap.c 生成 client
readn:
读 N 个字节
readline:
读一行
read 函数的返回值:
1. > 0 实际读到的字节数
2. = 0 已经读到结尾(对端已经关闭)【 !重 !点 !】
3. -1 应进一步判断errno的值:
errno = EAGAIN or EWOULDBLOCK: 设置了非阻塞方式 读。 没有数据到达。
errno = EINTR 慢速系统调用被 中断。
errno = “其他情况” 异常。
37P-多进程并发服务器思路分析
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1)
cfd = Accpet(); 接收客户端连接请求。
pid = fork();
if (pid == 0) 子进程 read(cfd) --- 小-》大 --- write(cfd)
close(lfd) 关闭用于建立连接的套接字 lfd
read()
小--大
write()
else if (pid > 0)
close(cfd); 关闭用于与客户端通信的套接字 cfd
contiue;
5. 子进程:
close(lfd)
read()
小--大
write()
父进程:
close(cfd);
注册信号捕捉函数: SIGCHLD
在回调函数中, 完成子进程回收
while (waitpid());
38P-多线程并发服务器分析
多线程并发服务器: server.c
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1)
cfd = Accept(lfd, );
pthread_create(&tid, NULL, tfn, (void *)cfd);
pthread_detach(tid); // pthead_join(tid, void **); 新线程---专用于回收子线程。
5. 子线程:
void *tfn(void *arg)
// close(lfd) 不能关闭。 主线程要使用lfd
read(cfd)
小--大
write(cfd)
pthread_exit((void *)10);
39P-多进程并发服务器实现
第一个版本的代码如下:
- #include <stdio.h>
- #include <ctype.h>
- #include <stdlib.h>
- #include <sys/wait.h>
- #include <string.h>
- #include <strings.h>
- #include <unistd.h>
- #include <errno.h>
- #include <signal.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <pthread.h>
- #include “wrap.h”
- #define SRV_PORT 9999
- int main(int argc, char *argv[])
-
int lfd, cfd;
-
pid_t pid;
-
struct sockaddr_in srv_addr, clt_addr;
-
socklen_t clt_addr_len;
-
char buf[BUFSIZ];
-
int ret, i;
-
//memset(&srv_addr, 0, sizeof(srv_addr)); // 将地址结构清零
-
bzero(&srv_addr, sizeof(srv_addr));
-
srv_addr.sin_family = AF_INET;
-
srv_addr.sin_port = htons(SRV_PORT);
-
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
-
lfd = Socket(AF_INET, SOCK_STREAM, 0);
-
Bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
-
Listen(lfd, 128);
-
clt_addr_len = sizeof(clt_addr);
-
while (1)
-
cfd = Accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
-
pid = fork();
-
if (pid < 0)
-
perr_exit("fork error");
-
else if (pid == 0)
-
close(lfd);
-
break;
-
else
-
close(cfd);
-
continue;
-
-
-
if (pid == 0)
-
for (;;)
-
ret = Read(cfd, buf, sizeof(buf));
-
if (ret == 0)
-
close(cfd);
-
exit(1);
-
-
for (i = 0; i < ret; i++)
-
buf[i] = toupper(buf[i]);
-
write(cfd, buf, ret);
-
write(STDOUT_FILENO, buf, ret);
-
-
-
return 0;
编译运行,结果如下:
这个代码,有问题。我们Ctrl+C终止一个连接进程,会发现,有僵尸进程。
如上图所示,有个僵尸进程。这是因为父进程在阻塞等待,没来得及去回收这个子进程。
所以需要修改代码,增加子进程回收,用信号捕捉来实现。
修改部分如图所示:
完整代码如下:
- #include <stdio.h>
- #include <ctype.h>
- #include <stdlib.h>
- #include <sys/wait.h>
- #include <string.h>
- #include <strings.h>
- #include <unistd.h>
- #include <errno.h>
- #include <signal.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <pthread.h>
- #include “wrap.h”
- #define SRV_PORT 9999
- void catch_child(int signum)
-
while ((waitpid(0, NULL, WNOHANG)) > 0);
-
return ;
- int main(int argc, char *argv[])
-
int lfd, cfd;
-
pid_t pid;
-
struct sockaddr_in srv_addr, clt_addr;
-
socklen_t clt_addr_len;
-
char buf[BUFSIZ];
-
int ret, i;
-
//memset(&srv_addr, 0, sizeof(srv_addr)); // 将地址结构清零
-
bzero(&srv_addr, sizeof(srv_addr));
-
srv_addr.sin_family = AF_INET;
-
srv_addr.sin_port = htons(SRV_PORT);
-
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
-
lfd = Socket(AF_INET, SOCK_STREAM, 0);
-
Bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
-
Listen(lfd, 128);
-
clt_addr_len = sizeof(clt_addr);
-
while (1)
-
cfd = Accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
-
pid = fork();
-
if (pid < 0)
-
perr_exit("fork error");
-
else if (pid == 0)
-
close(lfd);
-
break;
-
else
-
struct sigaction act;
-
act.sa_handler = catch_child;
-
sigemptyset(&act.sa_mask);
-
act.sa_flags = 0;
-
ret = sigaction(SIGCHLD, &act, NULL);
-
if (ret != 0)
-
perr_exit("sigaction error");
-
-
close(cfd);
-
continue;
-
-
-
if (pid == 0)
-
for (;;)
-
ret = Read(cfd, buf, sizeof(buf));
-
if (ret == 0)
-
close(cfd);
-
exit(1);
-
-
for (i = 0; i < ret; i++)
-
buf[i] = toupper(buf[i]);
-
write(cfd, buf, ret);
-
write(STDOUT_FILENO, buf, ret);
-
-
-
return 0;
这样,当子进程退出时,父进程收到信号,就会去回收子进程了,不会出现僵尸进程。
40P-多进程服务器测试IP地址调整
使用桥接模式,让自己主机和其他人主机处于同一个网段
41P-服务器程序上传外网服务器并访问
scp -r 命令,将本地文件拷贝至远程服务器上目标位置
scp -r 源地址 目标地址
42P-多线程服务器代码review
代码如下:
- #include <stdio.h>
- #include <string.h>
- #include <arpa/inet.h>
- #include <pthread.h>
- #include <ctype.h>
- #include <unistd.h>
- #include <fcntl.h>
- #include “wrap.h”
- #define MAXLINE 8192
- #define SERV_PORT 8000
- struct s_info //定义一个结构体, 将地址结构跟cfd捆绑
-
struct sockaddr_in cliaddr;
-
int connfd;
- ;
- void *do_work(void *arg)
-
int n,i;
-
struct s_info *ts = (struct s_info*)arg;
-
char buf[MAXLINE];
-
char str[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16 可用"[+d"查看
-
while (1)
-
n = Read(ts->connfd, buf, MAXLINE); //读客户端
-
if (n == 0)
-
printf("the client %d closed...\\n", ts->connfd);
-
break; //跳出循环,关闭cfd
-
-
printf("received from %s at PORT %d\\n",
-
inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
-
ntohs((*ts).cliaddr.sin_port)); //打印客户端信息(IP/PORT)
-
for (i = 0; i < n; i++)
-
buf[i] = toupper(buf[i]); //小写-->大写
-
Write(STDOUT_FILENO, buf, n); //写出至屏幕
-
Write(ts->connfd, buf, n); //回写给客户端
-
-
Close(ts->connfd);
-
return (void *)0;
- int main(void)
-
struct sockaddr_in servaddr, cliaddr;
-
socklen_t cliaddr_len;
-
int listenfd, connfd;
-
pthread_t tid;
-
struct s_info ts[256]; //创建结构体数组.
-
int i = 0;
-
listenfd = Socket(AF_INET, SOCK_STREAM, 0); //创建一个socket, 得到lfd
-
bzero(&servaddr, sizeof(servaddr)); //地址结构清零
-
servaddr.sin_family = AF_INET;
-
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //指定本地任意IP
-
servaddr.sin_port = htons(SERV_PORT); //指定端口号
-
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //绑定
-
Listen(listenfd, 128); //设置同一时刻链接服务器上限数
-
printf("Accepting client connect ...\\n");
-
while (1)
-
cliaddr_len = sizeof(cliaddr);
-
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); //阻塞监听客户端链接请求
-
ts[i].cliaddr = cliaddr;
-
ts[i].connfd = connfd;
-
pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
-
pthread_detach(tid); //子线程分离,防止僵线程产生.
-
i++;
-
-
return 0;
编译运行,结果如下:
43P-read返回值和总结
三次握手:
主动发起连接请求端,发送 SYN 标志位,请求建立连接。 携带序号号、数据字节数(0)、滑动窗口大小。
被动接受连接请求端,发送 ACK 标志位,同时携带 SYN 请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小。
主动发起连接请求端,发送 ACK 标志位,应答服务器连接请求。携带确认序号。
四次挥手:
主动关闭连接请求端, 发送 FIN 标志位。
被动关闭连接请求端, 应答 ACK 标志位。 ----- 半关闭完成。
被动关闭连接请求端, 发送 FIN 标志位。
主动关闭连接请求端, 应答 ACK 标志位。 ----- 连接全部关闭
滑动窗口:
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。
错误处理函数:
封装目的:
在 server.c 编程过程中突出逻辑,将出错处理与逻辑分开,可以直接跳转man手册。
【wrap.c】 【wrap.h】
存放网络通信相关常用 自定义函数 存放 网络通信相关常用 自定义函数原型(声明)。
命名方式:系统调用函数首字符大写, 方便查看man手册
如:Listen()、Accept();
函数功能:调用系统调用函数,处理出错场景。
在 server.c 和 client.c 中调用 自定义函数
联合编译 server.c 和 wrap.c 生成 server
client.c 和 wrap.c 生成 client
readn:
读 N 个字节
readline:
读一行
read 函数的返回值:
1. > 0 实际读到的字节数
2. = 0 已经读到结尾(对端已经关闭)【 !重 !点 !】
3. -1 应进一步判断errno的值:
errno = EAGAIN or EWOULDBLOCK: 设置了非阻塞方式 读。 没有数据到达。
errno = EINTR 慢速系统调用被 中断。
errno = “其他情况” 异常。
多进程并发服务器:server.c
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1)
cfd = Accpet(); 接收客户端连接请求。
pid = fork();
if (pid == 0) 子进程 read(cfd) --- 小-》大 --- write(cfd)
close(lfd) 关闭用于建立连接的套接字 lfd
read()
小--大
write()
else if (pid > 0)
close(cfd); 关闭用于与客户端通信的套接字 cfd
contiue;
5. 子进程:
close(lfd)
read()
小--大
write()
父进程:
close(cfd);
注册信号捕捉函数: SIGCHLD
在回调函数中, 完成子进程回收
while (waitpid());
多线程并发服务器: server.c
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1)
cfd = Accept(lfd, );
pthread_create(&tid, NULL, tfn, (void *)cfd);
pthread_detach(tid); // pthead_join(tid, void **); 新线程---专用于回收子线程。
5. 子线程:
void *tfn(void *arg)
// close(lfd) 不能关闭。 主线程要使用lfd
read(cfd)
小--大
write(cfd)
pthread_exit((void *)10);
44P-复习
45P-TCP状态-主动发起连接
46P-TCP状态-主动关闭连接
47P-TCP状态-被动接收连接
48P-TCP状态-被动关闭连接
49P-2MSL时长
50P-TCP状态-其他状态
netstat -apn | grep client 查看客户端网络连接状态
netstat -apn | grep port 查看端口的网络连接状态
TCP状态时序图:
结合三次握手、四次挥手 理解记忆。
1. 主动发起连接请求端: CLOSE -- 发送SYN -- SEND_SYN -- 接收 ACK、SYN -- SEND_SYN -- 发送 ACK -- ESTABLISHED(数据通信态)
2. 主动关闭连接请求端: ESTABLISHED(数据通信态) -- 发送 FIN -- FIN_WAIT_1 -- 接收ACK -- FIN_WAIT_2(半关闭)
-- 接收对端发送 FIN -- FIN_WAIT_2(半关闭)-- 回发ACK -- TIME_WAIT(只有主动关闭连接方,会经历该状态)
-- 等 2MSL时长 -- CLOSE
3. 被动接收连接请求端: CLOSE -- LISTEN -- 接收 SYN -- LISTEN -- 发送 ACK、SYN -- SYN_RCVD -- 接收ACK -- ESTABLISHED(数据通信态)
4. 被动关闭连接请求端: ESTABLISHED(数据通信态) -- 接收 FIN -- ESTABLISHED(数据通信态) -- 发送ACK
-- CLOSE_WAIT (说明对端【主动关闭连接端】处于半关闭状态) -- 发送FIN -- LAST_ACK -- 接收ACK -- CLOSE
重点记忆: ESTABLISHED、FIN_WAIT_2 <--> CLOSE_WAIT、TIME_WAIT(2MSL)
netstat -apn | grep 端口号
2MSL时长:
一定出现在【主动关闭连接请求端】。 --- 对应 TIME_WAIT 状态。
保证,最后一个 ACK 能成功被对端接收。(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)
51P-端口复用函数
52P-半关闭及shutdown函数
端口复用:
int opt = 1; // 设置端口复用。
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
半关闭:
通信双方中,只有一端关闭通信。 --- FIN_WAIT_2
close(cfd);
shutdown(int fd, int how);
how: SHUT_RD 关读端
SHUT_WR 关写端
SHUT_RDWR 关读写
shutdown在关闭多个文件描述符应用的文件时,采用全关闭方法。close,只关闭一个。
53P-多路IO转接服务器设计思路
54P-select函数参数简介
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1
readfds: 读 文件描述符监听集合。 传入、传出参数
writefds:写 文件描述符监听集合。 传入、传出参数 NULL
exceptfds:异常 文件描述符监听集合 传入、传出参数 NULL
timeout: > 0: 设置监听超时时长。
NULL: 阻塞监听
0: 非阻塞监听,轮询
返回值:
> 0: 所有监听集合(3个)中, 满足对应事件的总数。
0: 没有满足监听条件的文件描述符
-1: errno
55P-中午复习
56P-select函数原型分析
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1
readfds: 读 文件描述符监听集合。 传入、传出参数
writefds:写 文件描述符监听集合。 传入、传出参数 NULL
exceptfds:异常 文件描述符监听集合 传入、传出参数 NULL
timeout: > 0: 设置监听超时时长。
NULL: 阻塞监听
0: 非阻塞监听,轮询
返回值:
> 0: 所有监听集合(3个)中, 满足对应事件的总数。
0: 没有满足监听条件的文件描述符
-1: errno
57P-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) 位图所有二进制位置零
select多路IO转接:
原理: 借助内核, select 来监听, 客户端连接、数据通信事件。
void FD_ZERO(fd_set *set); --- 清空一个文件描述符集合。
fd_set rset;
FD_ZERO(&rset);
void FD_SET(int fd, fd_set *set); --- 将待监听的文件描述符,添加到监听集合中
FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset);
void FD_CLR(int fd, fd_set *set); --- 将一个文件描述符从监听集合中 移除。
FD_CLR(4, &rset);
int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。
返回值: 在:1;不在:0;
FD_ISSET(4, &rset);
58P-select实现多路IO转接设计思路
思路分析:
int maxfd = 0;
lfd = socket() ; 创建套接字
maxfd = lfd;
bind(); 绑定地址结构
listen(); 设置监听上限
fd_set rset, allset; 创建r监听集合
FD_ZERO(&allset); 将r监听集合清空
FD_SET(lfd, &allset); 将 lfd 添加至读集合中。
while(1)
rset = allset; 保存监听集合
ret = select(lfd+1, &rset, NULL, NULL, NULL); 监听文件描述符集合对应事件。
if(ret > 0) 有监听的描述符满足对应事件
if (FD_ISSET(lfd, &rset)) // 1 在。 0不在。
cfd = accept(); 建立连接,返回用于通信的文件描述符
maxfd = cfd;
FD_SET(cfd, &allset); 添加到监听通信描述符集合中。
for (i = lfd+1; i <= 最大文件描述符; i++)
FD_ISSET(i, &rset) 有read、write事件
read()
小 -- 大
write();
59P-select实现多路IO转接-代码review
代码如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <string.h>
- #include <arpa/inet.h>
- #include <ctype.h>
- #include “wrap.h”
- #define SERV_PORT 6666
- int main(int argc, char *argv[])
-
int i, j, n, nready;
-
int maxfd = 0;
-
int listenfd, connfd;
-
char buf[BUFSIZ]; /* #define INET_ADDRSTRLEN 16 */
-
struct sockaddr_in clie_addr, serv_addr;
-
socklen_t clie_addr_len;
-
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
-
int opt = 1;
-
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
bzero(&serv_addr, sizeof(serv_addr));
-
serv_addr.sin_family= AF_INET;
-
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
-
serv_addr.sin_port= htons(SERV_PORT);
-
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
-
Listen(listenfd, 128);
-
fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */
-
maxfd = listenfd;
-
FD_ZERO(&allset);
-
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
-
while (1)
-
rset = allset; /* 每次循环时都从新设置select监控信号集 */
-
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
-
if (nready < 0)
-
perr_exit("select error");
-
if (FD_ISSET(listenfd, &rset)) /* 说明有新的客户端链接请求 */
-
clie_addr_len = sizeof(clie_addr);
-
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
-
FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
-
if (maxfd < connfd)
-
maxfd = connfd;
-
if (0 == --nready) /* 只有listenfd有事件, 后续的 for 不需执行 */
-
continue;
-
-
for (i = listenfd+1; i <= maxfd; i++) /* 检测哪个clients 有数据就绪 */
-
if (FD_ISSET(i, &rset))
-
if ((n = Read(i, buf, sizeof(buf))) == 0) /* 当client关闭链接时,服务器端也关闭对应链接 */
-
Close(i);
-
FD_CLR(i, &allset); /* 解除select对此文件描述符的监控 */
-
else if (n > 0)
-
for (j = 0; j < n; j++)
-
buf[j] = toupper(buf[j]);
-
Write(i, buf, n);
-
-
-
-
-
Close(listenfd);
-
return 0;
编译运行,结果如下:
如图,借助select也可以实现多线程
60P-select实现多路IO转接-代码实现
61P-select实现多路IO转接-添加注释
代码太长了,直接看59话吧
62P-select优缺点
select优缺点:
缺点: 监听上限受文件描述符限制。 最大 1024.
检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。
优点: 跨平台。win、linux、macOS、Unix、类Unix、mips
select代码里有个可以优化的地方,用数组存下文件描述符,这样就不需要每次扫描一大堆无关文件描述符了
63P-添加一个自定义数组提高效率
这里就是改进之前代码的问题,之前的代码,如果最大fd是1023,每次确定有事件发生的fd时,就要扫描3-1023的所有文件描述符,这看起来很蠢。于是定义一个数组,把要监听的文件描述符存下来,每次扫描这个数组就行了。看起来科学得多。
如图,加个client数组,存要监听的描述符。
代码如下,挺长的
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <string.h>
- #include <arpa/inet.h>
- #include <ctype.h>
- #include “wrap.h”
- #define SERV_PORT 6666
- int main(int argc, char *argv[])
-
int i, j, n, maxi;
-
int nready, client[FD_SETSIZE]; /* 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */
-
int maxfd, listenfd, connfd, sockfd;
-
char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
-
struct sockaddr_in clie_addr, serv_addr;
-
socklen_t clie_addr_len;
-
fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */
-
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
-
int opt = 1;
-
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
bzero(&serv_addr, sizeof(serv_addr));
-
serv_addr.sin_family= AF_INET;
-
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
-
serv_addr.sin_port= htons(SERV_PORT);
-
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
-
Listen(listenfd, 128);
-
maxfd = listenfd; /* 起初 listenfd 即为最大文件描述符 */
-
maxi = -1; /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
-
for (i = 0; i < FD_SETSIZE; i++)
-
client[i] = -1; /* 用-1初始化client[] */
-
FD_ZERO(&allset);
-
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
-
while (1)
-
rset = allset; /* 每次循环时都重新设置select监控信号集 */
-
nready = select(maxfd+1, &rset, NULL, NULL, NULL); //2 1--lfd 1--connfd
-
if (nready < 0)
-
perr_exit("select error");
-
if (FD_ISSET(listenfd, &rset)) /* 说明有新的客户端链接请求 */
-
clie_addr_len = sizeof(clie_addr);
-
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
-
printf("received from %s at PORT %d\\n",
-
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
-
ntohs(clie_addr.sin_port));
-
for (i = 0; i < FD_SETSIZE; i++)
-
if (client[i] < 0) /* 找client[]中没有使用的位置 */
<
以上是关于Linux网络编程——黑马程序员笔记的主要内容,如果未能解决你的问题,请参考以下文章
黑马程序员 C++教程从0到1入门编程笔记3C++核心编程(内存分区模型引用函数提高)
黑马程序员 C++教程从0到1入门编程笔记5C++核心编程(类和对象——继承多态)