前言: 周末学了两天网络编程, 把之前的不懂一些问题基本掌握了, 例如 TCP 状态转换图, close 和 shutdown 函数的区别, select 函数等, 今天分享给大家.
一, 网络编程基础知识
在写代码之前, 需要简单介绍一下基础知识.
网络字节序
小端法(本地): 低地址存低字节, 高地址存高字节(简称高存高, 低存低)
大端法(网络): 高存底, 低存高
可能有人会问为啥不统一呢? 历史遗留问题: IBM 最开始使用数据存储使用大端法, 后来微软非要反着来.
所以通信必须先统一字节序, 涉及 4 个函数, 如下:(函数原型都是通过 man 手册查的)
- #include <arpa/inet.h>
- uint32_thtonl(uint32_t hostlong);
- uint16_t htons(uint16_t hostshort);
- uint32_t ntohl(uint32_t netlong);
- uint16_t ntohs(uint16_t netshort);
说明: h 代表本地, n 代表网络, l 表示 32 长整数(也是历史遗留问题, 最开始没有 int 型),s 表示 16 位短整型
例如 htonl 函数: 就是将本地字节序转为网路字节序, 并且是长整数, 一般用在 ip 转换, s 则用在端口.
2.sockaddr 地址结构
通用地址结构体:
- struct sockaddr {
- sa_family_t sa_family;// 协议族 AF_INET AF_INET6
- char sa_data[14];//1-2 端口号 3-6 ip 地址 7-14 预留
- }
ipv4 专用地址结构:
- struct sockaddr_in {
- sa_family_t sin_family; /* address family: AF_INET */
- in_port_t sin_port; /* port in network byte order */
- struct in_addr sin_addr; /* internet address */
- };
- /* Internet address. */
- struct in_addr {
- uint32_t s_addr; /* address in network byte order */
- };
通用的使用麻烦, 弄一个专用的方便了, 但是 bind 函数原型如下:
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
要传通用地址结构, 所以使用时就要进行强转.
其实, 还应该介绍一下 socket,bind,listen 等函数, 但是觉得很简单就先不介绍了.
二, 三次握手和四次挥手
这篇主要介绍 TCP,TCP 经常被问到的问题就是三次握手, 应该大部分人都知道, 但四次挥手应该很少人知道, 确很重要对理解一下概念和函数很有帮助.
1. 三次握手
三次握手如下图:(截图《unix 网络编程》, 简称 UNP, 下面还有有图出自这本书)
说明: 客户端发送 SYN 请求, 服务器收到之后, 返回 ACK 应答并携带 SYN, 客户端收到之后发送 ACK 应答. 更多解释可以参考 UNP.
2. 四次挥手
四次挥手如下图, 出自 UNP:
说明: 客户端发起 FIN 关闭请求, 服务器收到之后回应 ACK, 到这完成了 "半关闭", 就是关了一半. 为啥只关了一半? 其实客户端, 服务器各有一个描述符, 两个缓冲区(读缓冲, 写缓冲), 关的只是一个缓冲区.
服务端再发 FIN, 客户端收到之后再回应 ACK 应答.
理解四次挥手, 对 close 和 shutdown 函数的区别会有很好的理解.
三, TCP 状态转换图
理解三次握手, 四次挥手, 对于面试, 跟人装逼很有帮助, 开玩笑了. 但理解 TCP 状态转换图对编程很有帮助.
TCP 状态转换图如下, 也出自 UNP:
说明: 如图释一样, 客户端状态走向, 虚线服务器的走向, 要分别沿着两条线去看, 并结合着三次握手和四次挥手去看. 在纸上画过 2 个结合的图, 但是没时间在电脑上, 有需要的话联系我吧.
shutdowan,close 函数区别:
shutdown 原型: int shutdown(int sockfd, int how);
参数说明:
sockfd: 文件描述符
how: 定义三个宏
SHUT_RD 关闭读端
SHUT_WR 关闭写端
SHUT_RDWR 关闭读写端
close 原型: int close(int fd); 参数文件描述符.
还有一个重要区别: shutdown 在关闭多个文件描述符时, 采用全关闭方法. close 只关闭一个, 相当于 "-1" 操作.
四, select 函数介绍
select 函数非常复杂, 一点点解释吧, 函数原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数说明:
nfds: 监听的所有文件描述符中的最大描述符 + 1(其实内核是轮询查的)
readfds: 读文件描述符监听集合
writefds: 写文件描述符监听集合
exceptfds: 异常文件描述符监听集合
timeout: 有几个值如下:
大于 0: 设置监听超时时长
NULL: 阻塞监听
0: 非阻塞监听
函数返回值:
大于 0: 所有监听集合 (3 个) 中, 满足对应事件的总数
0: 没有满足的
-1: 出错 error
看第二个, 三个, 四个参数的类型 fd_set, 内核为操作这种集合定义了四个函数, 如下:
- void FD_CLR(int fd, fd_set *set); // 将一个文件描述符从集合中移除
- int FD_ISSET(int fd, fd_set *set); // 判断一个文件描述符是否在一个集合中, 返回值: 在 1, 不在 0
- void FD_SET(int fd, fd_set *set); // 将监听的文件描述符, 添加到监听集合中
- void FD_ZERO(fd_set *set); // 清空一个文件描述符集合
五, 包裹函数
这个概念来自 UNP, 先介绍包裹函数的定义, 约定的包裹函数名是实际函数的首字母大写形式. 例如, 如下:
Socket(int family, int type, int protocol)
为什么要怎么做呢? 原因如下:
(1) 检查返回值
(2)独立错误检查模块
六, 用 select 实现并发服务器
服务端
socket,bind,listen 这些都没变化, 但都用的包裹函数形式, 封装在 wrap.c 中, 从 accept 开始就要开始处理了, 程序中都有注释, 不明白在私信我, 先主要讲解一下, 调用 FD_ZERO,FD_SET 设置 fd_set 集合, 再监听连接, 再监听数据传输. 代码如下:
- #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);
- 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[]中没有使用的位置 */
- client[i] = connfd; /* 保存 accept 返回的文件描述符到 client[]里 */
- break;
- }
- if (i == FD_SETSIZE) { /* 达到 select 能监控的文件个数上限 1024 */
- fputs("too many clients\n", stderr);
- exit(1);
- }
- FD_SET(connfd, &allset); /* 向监控文件描述符集合 allset 添加新的文件描述符 connfd */
- if (connfd> maxfd)
- maxfd = connfd; /* select 第一个参数需要 */
- if (i> maxi)
- maxi = i; /* 保证 maxi 存的总是 client[]最后一个元素下标 */
- if (--nready == 0)
- continue;
- }
- for (i = 0; i <= maxi; i++) { /* 检测哪个 clients 有数据就绪 */
- if ((sockfd = client[i]) <0)
- continue;
- if (FD_ISSET(sockfd, &rset)) {
- if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { /* 当 client 关闭链接时, 服务器端也关闭对应链接 */
- Close(sockfd);
- FD_CLR(sockfd, &allset); /* 解除 select 对此文件描述符的监控 */
- client[i] = -1;
- } else if (n> 0) {
- for (j = 0; j <n; j++)
- buf[j] = toupper(buf[j]);
- Write(sockfd, buf, n);
- Write(STDOUT_FILENO, buf, n);
- }
- if (--nready == 0)
- break; /* 跳出 for, 但还在 while 中 */
- }
- }
- }
- Close(listenfd);
- return 0;
- }
- View Code
客户端
实现简单功能: 客户端发小写, 服务器转为大写再返回给客户端. 客户端首先 socket,connect, 依然是包裹函数. 代码如下:
- /* client.c */
- #include <stdio.h>
- #include <string.h>
- #include <unistd.h>
- #include <arpa/inet.h>
- #include <netinet/in.h>
- #include "wrap.h"
- #define MAXLINE 80
- #define SERV_PORT 6666
- int main(int argc, char *argv[])
- {
- struct sockaddr_in servaddr;
- char buf[MAXLINE];
- int sockfd, n;
- if (argc != 2)
- printf("./client IP\n");
- sockfd = Socket(AF_INET, SOCK_STREAM, 0);
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
- servaddr.sin_port = htons(SERV_PORT);
- Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
- printf("------------connect ok----------------\n");
- while (fgets(buf, MAXLINE, stdin) != NULL) {
- Write(sockfd, buf, strlen(buf));
- n = Read(sockfd, buf, MAXLINE);
- if (n == 0) {
- printf("the other side has been closed.\n");
- break;
- }
- else
- Write(STDOUT_FILENO, buf, n);
- }
- Close(sockfd);
- return 0;
- }
- View Code
这样就可以了, 想要《unix 网络编程》,wrap.c 的, 推荐评论我.
其实, TCP 状态转换图, select 实现原理, 应该用画图来解释一下, 今天 7 点多就到公司了, 准备写博客, 然后 9 点就去处理需求了, 下午才写完, 等有时间再详细介绍这两方面吧.
最后, 评论你的不懂问题, 需要资料的也随时欢迎评论.
来源: https://www.cnblogs.com/liudw-0215/p/9661583.html