一二章请点击:网络编程_1(网络基础+跨主机传输)
三四章请点击:网络编程_2(网络属性+UDP(UDP模型+广播组播))
第五章请点击:网络编程_3(TCP)
第六章请点击:网络编程_4(IO模型)
七、超时检测
1.在网络通讯中,有很多函数是阻塞函数,使进程阻塞。例如 accpet recv send recvfrom sendto等等。
2.为了避免进程在阻塞函数处无限制阻塞,可以设定超时时间,当超时后从阻塞函数立即返回,继续运行。
超时检测方法
1.select 可以通过函数设置超时时间 struct timeval;
struct timeval val= {5, 0};
ret = select(fd+1, &temp, NULL, NULL, &val);
2.通过setsockopt设置超时时间;
struct timeval val = {5, 0};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &val, sizeof(val)); //设置接收超时时间;
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &val, sizeof(val)); //设置发送超时时间;
设置超时时间后,recv函数和send函数会返 -1 , errno = EAGAIN or EWOULDBLOCK
3.使用alarm(3)定时器设置超时时间;
signal(SIGALRM, handler);
void handler(int sig)
{
printf("超时\n");
}
八、域套接字
仅用于本机间的通信,bsp-lcd中的s类型
1.socket
头文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
原型:
int socket(int domain, int type, int protocol);
fd = socket(AF_UNIX, SOCK_STREAM, 0);
fd = socket(AF_UNIX, SOCK_DGRAM, 0);
//struct sockaddr_in ipv4 AF_INET的结构体
AF_UNIX:(man 7 unix)
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */ 套接字的文件路径(绝对路径)/home/linux/sock
注意:在服务器中,这个套接字的路径必须事先不存在,需要有bind函数产生(该文件必须由服务器产生);
如果事先存在该路径文件,需要先删除该文件。
};
2.access
功能:判断文件是否存在以及文件是否具有某些权限;
头文件:
#include <unistd.h>
原型:
int access(const char *pathname, int mode);
参数:
const char *pathname:指定要判断的文件路径+文件名;
int mode:
R_OK:判断文件是否具有可读权限
W_OK:判断文件是否具有可写权限
X_OK:判断文件是否具有可执行权限
------前三种,可以用按位或| 连接 ————————
F_OK:判断文件是否存在;
返回值:
成功,返回0;
失败,返回-1,更新errno;
3.unlink
功能:删除文件;
头文件:
#include <unistd.h>
原型:
int unlink(const char *pathname);
参数:
char *pathname:指定要删除的文件;
返回值:
成功,返回0;
失败,返回-1,更新errno;
例子
客户端和服务器两边创建套接字时指定为UNIX编程即可,若选择SOCK_STREAM则其他同TCP,若选择SOCK_D则其他同UDP
服务器
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/un.h>
#include <unistd.h>
#define PATH "/home/linux/mydir/7_20101_socket/7_unix/unix_sock"
void cli_info(int fd, struct sockaddr_in cin);
int updatefds(int maxfd, fd_set readfds);
int main(int argc, const char *argv[])
{
//1.创建套接字
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(sfd < 0)
{
perror("socket");
return -1;
}
//允许本地端口快速重用
int reuse = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int));
//判断路径是否存在
//如果存在则删除文件
if(!access(PATH, F_OK))
{
fprintf(stderr, "文件已经存在\n");
unlink(PATH);
printf("文件已经删除\n");
}
//2.bind
//填充服务器信息
struct sockaddr_un sun;
sun.sun_family = AF_UNIX;
strcpy(sun.sun_path , PATH);
if(bind(sfd, (void*)&sun, sizeof(sun))<0)
{
perror("bind");
exit(1);
}
//3.监听
if(listen(sfd, 3)<0)
{
perror("listen");
exit(1);
}
//创建集合
fd_set readfds, temp;
FD_ZERO(&readfds);
FD_ZERO(&temp);
FD_SET(sfd, &readfds); //监听套接字
FD_SET(0, &readfds); //键盘事件
int ret = -1;
char buf[BUFSIZ] = "";
struct timeval tv = {5, 0};
int maxfd = sfd;
while(1)
{
tv.tv_sec = 5;
temp = readfds;
ret = select(maxfd+1, &temp, NULL, NULL, NULL);
if(ret < 0)
{
perror("select");
exit(1);
}
else if(0 == ret)
{
fprintf(stderr, "time out\n");
continue;
}
int i = 0;
for(i=0; i < maxfd+1; i++)
{
if(!FD_ISSET(i, &temp))
{
continue;
}
if(0 == i)
{
//键盘事件
bzero(buf, sizeof(buf));
int sendfd = -1;
int ret = scanf("%d %s", &sendfd, buf);
while(getchar()!='\n');
if(ret != 2)
{
fprintf(stderr, "输入错误: fd+string\n");
continue;
}
if(!FD_ISSET(sendfd, &readfds))
{
fprintf(stderr, "fd=%d不在集合中\n", sendfd);
continue;
}
do
{
ret = send(sendfd, buf, strlen(buf), 0);
}while(ret<0 && EINTR == errno);
if(ret < 0)
{
perror("send");
close(sendfd);
FD_CLR(sendfd, &readfds);
maxfd = updatefds(maxfd, readfds);
}
printf("发送成功\n");
}
else if(sfd == i)
{
//客户端连接事件
int newfd = accept(sfd, NULL, NULL);
if(newfd < 0)
{
perror("accept");
exit(1);
}
printf("fd=%d客户端连接\n", newfd);
//将newfd添加到readfds中
FD_SET(newfd, &readfds);
maxfd = maxfd>newfd?maxfd:newfd;
}
else
{
//客户端交互事件
bzero(buf, sizeof(buf));
do
{
ret = recv(i, buf, sizeof(buf), 0);
}while(ret<0 && EINTR==errno);
if(ret < 0)
{
perror("recv");
FD_CLR(i, &readfds);
close(i);
//更新maxfd
maxfd = updatefds(maxfd, readfds);
}
else if(0 == ret)
{
FD_CLR(i, &readfds);
close(i);
printf("fd=%d 已断开链接\n", i);
//更新maxfd
maxfd = updatefds(maxfd, readfds);
}
else
{
printf("收到来自%d的消息:%s\n", i, buf);
}
}
}
}
close(sfd);
return 0;
}
int updatefds(int maxfd, fd_set readfds)
{
int i = 0;
for(i=maxfd; i>2; i--)
{
if(FD_ISSET(i, &readfds))
{
return i;
}
}
return -1;
}
客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/un.h>
#include <unistd.h>
#define PATH "/home/linux/mydir/7_20101_socket/7_unix/unix_sock"
int main(int argc, const char *argv[])
{
//1.创建套接字
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if(fd < 0)
{
perror("socket");
return -1;
}
//允许本地端口快速重用
int reuse = 1;
int len = sizeof(reuse);
if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, len)<0)
{
perror("setsockopt");
exit(1);
}
//2.绑定 非必须
//3.连接服务器
//填充服务器信
struct sockaddr_un sun;
sun.sun_family = AF_UNIX;
strcpy(sun.sun_path, PATH);
//int connect(int sockfd, \
//const struct sockaddr *addr, socklen_t addrlen);
if(connect(fd, (void*)&sun, sizeof(sun))<0)
{
perror("connect");
exit(1);
}
printf("连接成功\n");
//创建集合
fd_set rfds, temp;
FD_ZERO(&rfds);
FD_ZERO(&temp);
FD_SET(0, &rfds);
FD_SET(fd, &rfds);
char buf[256] = "";
//4.发送消息
int ret = -1;
while(1)
{
temp = rfds;
ret = select(fd+1, &temp, NULL, NULL, NULL);
if(ret < 0)
{
perror("select");
exit(2);
}
else if(0 == ret)
{
fprintf(stderr, "超时\n");
continue;
}
//键盘事件,说明终端有数据输入
//可以用函数读取终端的数据,然后发送给服务器
if(FD_ISSET(0, &temp))
{
bzero(buf, sizeof(buf));
//fprintf(stderr, "请输入:");
fgets(buf, 256-1, stdin);
// ssize_t send(int sockfd, const void *buf, size_t len, int flags);
do
{
ret = send(fd, buf, strlen(buf), 0);
}while(ret<0 && errno == EINTR);
if(ret < 0)
{
perror("send");
exit(1);
}
}
//服务器交互事件,说明服务器有数据发送到客户端
//可以用recv函数读取数据
if(FD_ISSET(fd, &temp))
{
bzero(buf, sizeof(buf));
do
{
ret = recv(fd, buf, sizeof(buf), 0);
}while(ret<0 && EINTR==errno);
if(ret < 0)
{
perror("recv");
exit(1);
}
else if(0 == ret)
{
fprintf(stderr ,"-----服务器已关闭-----\n");
break;
}
printf("%s\n", buf);
}
}
close(fd);
return 0;
}
九、抓包工具
1.wireshark的概念
1.1概念
- wireshark是非常流行的网络封包分析软件,功能十分强大。
- 可以截取各种网络封包,显示网络封包的详细信息。
- wireshark是开源软件,可以放心使用。
- 可以运行在Windows和Mac OS上。
- 使用wireshark的人必须了解网络协议,否则就看不懂wireshark了。
1.2. Wireshark不能做的
为了安全考虑,wireshark只能查看封包,而不能修改封包的内容,或者发送封包。
1.3. Wireshark VS Fiddler
1.4. 同类的其他工具
1) 微软的network monitor
2) sniffer
1.5. wireshark的作用
- 网络管理员会使用wireshark来检查网络问题
- 软件测试工程师使用wireshark抓包,来分析自己测试的软件
- 从事socket编程的工程师会用wireshark来调试
- 总之跟网络相关的东西,都可能会用到wireshark.
2.安装
1) wireshark的官方下载网站:www.wireshark.org/
2) Linux sudo apt install wireshark
3) 启动sudo wireshark
设置中文 Edit->Preferences->Appaerance->Language->Chinese
3.wireshark 抓包
3.1开始抓包
开始界面
- wireshark是捕获机器上的某一块网卡的网络包,当你的机器上有多块网卡的时候,你需要选择一个网卡。
- 点击Caputre->Interfaces… 出现下面对话框,选择正确的网卡。然后点击"Start"按钮, 开始抓包
Wireshark 窗口介绍
WireShark 主要分为这几个界面
- Display Filter(显示过滤器), 用于过滤
- Packet List Pane(封包列表), 显示捕获到的封包, 有源地址和目标地址,端口号。 颜色不同,代表
- Packet Details Pane(封包详细信息), 显示封包中的字段
- Dissector Pane(16进制数据)
- Miscellanous(地址栏,杂项)
4.Wireshark 显示过滤
4.1 概念
- 使用过滤是非常重要的, 初学者使用wireshark时,将会得到大量的冗余信息,在几千甚至几万条记录中,以至于很难找到自己需要的部分。搞得晕头转向。
- 过滤器会帮助我们在大量的数据中迅速找到我们需要的信息。
- 过滤器有两种,
a) 一种是显示过滤器,就是主界面上那个,用来在捕获的记录中找到所需要的记录
b) 一种是捕获过滤器,用来过滤捕获的封包,以免捕获太多的记录。 在Capture -> Capture Filters 中设置
4.2 保存过滤
- 在Filter栏上,填好Filter的表达式后,点击Save按钮, 取个名字。比如"Filter 102",
- Filter栏上就多了个"Filter 102" 的按钮。
4.3 过滤表达式的规则
表达式规则
1) 协议过滤
比如TCP,只显示TCP协议。
2) IP 过滤
比如 ip.src 192.168.1.102 显示源地址为192.168.1.102,
ip.dst192.168.1.102, 目标地址为192.168.1.102
3) 端口过滤
tcp.port ==80, 端口为80的
tcp.srcport == 80, 只显示TCP协议的愿端口为80的。
4) http模式过滤
http.request.method==“GET”, 只显示HTTP GET方法的。
5) 逻辑运算符为 AND/ OR
常用的过滤表达式
过滤表达式 | 用途 |
---|---|
http | 只查看HTTP协议的记录 |
ip.src192.168.1.102 or ip.dst192.168.1.102 | 源地址或者目标地址是192.168.1.102 |
5.封包列表(Packet List Pane)
- 封包列表的面板中显示,编号,时间戳,源地址,目标地址,协议,长度,以及封包信息。
- 你可以看到不同的协议用了不同的颜色显示。
- 你也可以修改这些显示颜色的规则, View ->Coloring Rules.
6.封包详细信息 (Packet Details Pane)
这个面板是我们最重要的,用来查看协议中的每一个字段。
各行信息分别为
- Frame: 物理层的数据帧概况
- Ethernet II: 数据链路层以太网帧头部信息
- Internet Protocol Version 4: 互联网层IP包头部信息
- Transmission Control Protocol: 传输层的数据段头部信息,此处是TCP
- Hypertext Transfer Protocol: 应用层的信息,此处是HTTP协议
7.wireshark与对应的OSI七层模型
十、包头分析
1.以太网头
以太网头中封装了源mac地址以及目的mac地址,还有ip类型。以太网头又称之为mac头
2.IP头
TTL:time to live 生命周期,数据包能够经过几个路由器。当目标接包的时候,这个的ttl减到0就会被丢弃。
Linux:TTL64 Windows:TTL128.中国连接到世界任意一个点绝对够用。
所以丢包不是ttl耗尽造成的。
丢包是由于阻塞造成的,路由由一个等待队列,由丢包算法。
3.UDP头
source port 源端口号:2字节 16位
destination port 目标端口号:2字节
length 总长度:2字节
checksum 校验码:2字节
总共占8字节
4.TCP头
序号
序列号seq:占4个字节,用来标记数据段顺序
TCP把连接中发送的所有数据字节都编上号。
确认号:占4个字节,期待收到对方下一个报文段的第一个数据字节的序号
序列号表示报文携带数据的第一个字节编号;
确认好指期待收到下一个字节的编号。
TCP flag
确认ACK:占1位,当ACK=1,确认号才有效
同步SYN:连接简历是用同步序列号
SYN = 1 ,ACK = 0表示请求连接
终止FIN:用来释放一个连接,如果FIN=1此报文发送方数据已经发完了。
通过确认重发机制,做到可靠传输。
三次握手
三次握手:在客户端尝试连接服务器时候会产生
第一次握手:客户端发送syn包(SYN=1,seq=x)给服务器,并进入SYN_SEND状态,等待服务返回确认;
第二次握手:服务器接收到syn包,确认客户端的SYN,发送ack包(ACK=1,ack=x+1),同时发送一个syn包(SYN=1,seq=y),并进入SYN-RECV状态。
第三次握手:客户端收到服务器的syn以及ack包,向服务器发送ack包(ACK=1, ack=y+1),此时三次握手的包发送完毕,客户端和服务器进入ESTAB-LISHED状态。完成三次握手。
四次挥手
断开连接,可能是服务器发起的,也可能是客户端发起的
第一次挥手,主动关闭的一方发送一个FIN(FIN=1,seq=u)给被动方,进入FIN-WAIT-1状态;
第二次挥手,被动方接受到FIN包,给主动方发送一个ACK包(ACK=1, ack=1+u),并进入CLOSE-WAIT状态。主动方接收到ACk包,进入FIN-WAIT-2状态,如果有数据没有发送完毕,则继续发送,直到发送完毕;
第三次挥手,被动关闭方发送一个FIN包(FIN=1,seq=w),进入LAST-ACK状态。
第四次挥手,主动关闭方收到FIN包,回复一个ACK包(ACK=1,ack=w+1)。此时主动关闭方等待2个MSL(最大报文寿命 linux=60s)时间后关闭连接,被动关闭方收到主动关闭方的ACK后关闭连接
为什么要等待2个MSL?
被动方可能会收不到ack包,如果主动方发完ack包就退出的话,就无法重新在发一个ack包了,所以需要等待2个MSL,确保被动方收到ack包。