Linux使用的网络模型是TCP/IP四层网络模型,主要由应用程序、传输层、网络层、网络接口层组成。与OSI七层模型不同,但是又相互对应,它们之间关系如下图:
OSI模型的应用层、表示层、会话层对应着TCP/IP模型的应用层,传输层对应传输层,网络层对应网络互连层,数据链路层和物理层对应主机到网络层(网络接口层)。linux中的网卡驱动属于7层模型中的数据链路层,属于四层模型中的最底层的网络接口层。
Linux用到的网络协议主要有:TCP、IP、UDP、以太网协议等。这些协议之间的关系,体现在各类协议数据包之间的关系,主要是各类数据包之间的相互包含。如下图所示
TCP数据包加上IP首部之后,称为IP数据报,IP数据报加上以太网首部,以太网尾部成为以太网帧。网络上传输的所有数据都是以太网帧的形式。
Linux采用统一的网络编程模型:利用Socket(套接字)抽象层进行网络编程。如果不采用统一的编程模型,那么linux的网络编程会是下图的情况:
用户程序只需要调用socket抽象层提供的统一接口即可,无需考虑具体要使用哪个协议,这些事情内核会帮我们解决,我们只要调用socket抽象层提供的接口就行。这样,进程a、b、c都只要调用send方法就OK。这就使linux网络编程变得很方便了。socket又叫套接字编程模型。
是指多字节数据的存储顺序
分类
- 小端格式:将低位字节数据存储在低地址
- 大端格式:将高位字节数据存储在低地址
LSB:低地址 MSB:高地址
确定主机字节序程序
#include
union{
short s;
char c[sizeof(short)];
}un;
int main()
{
un.s =0x0102;
if((un.c[0]==1)&&(un.c[1]==2))
{
printf("big-endian\n");
}
if((un.c[0]==2)&&(un.c[1]==1))
{
printf("little-endian\n");
}
return 0;
}
将32位主机字节序数据转换成网络字节序数据
将主机字节序的IP地址转换成网络字节序
//头文件:
#include
uint32_t htonl(uint32_t hostint32);
/*
参数:
hostint32:待转换的32位主机字节序数据
返回值:
成功:返回网络字节序的值
*/
例
#include
#include
int main(int argc, char *argv[])
{
int num = 0x01020304;//
short a = 0x0102;
int sum = htonl(num);
printf("%x\n",sum);
short b = htons(a);
printf("%x\n",b);
return 0;
}
将16位主机字节序数据转换成网络字节序数据
将主机字节序的端口转换成网络字节序
//头文件:
#include
uint16_t htons(uint16_t hostint16);
/*
参数:
uint16_t:unsigned short int
hostint16:待转换的16位主机字节序数据
返回值:
成功:返回网络字节序的值
*/
将32位网络字节序数据转换成主机字节序数据
//头文件:
#include
uint32_t ntohl(uint32_t netint32);
/*
参数:
uint32_t: unsigned int
netint32:待转换的32位网络字节序数据
返回值:
成功:返回主机字节序的值
*/
例
#include
#include
int main(int argc, char *argv[])
{
int num = 0x01020304;//
int sum = htonl(num);
printf("%x\n",ntohl(sum));
return 0;
}
将16位网络字节序数据转换成主机字节序数据
//头文件:
#include
uint16_t ntohs(uint16_t netint16);
/*
参数:
uint16_t: unsigned short int
netint16:待转换的16位网络字节序数据
返回值:
成功:返回主机字节序的值
*/
例
#include
#include
int main(int argc, char *argv[])
{
short a = 0x0102;
short b = htons(a);
printf("%x\n",ntohs(b));
return 0;
}
字符串ip地址转整型数据
将点分十进制数串转换成32位无符号整数
//头文件:
#include
int inet_pton(int af,const char *stc, void *dst);
/*
参数:
af: 协议族 选IPV4对应的宏AF_INET ,选IPv6对应的宏AF_INET6
stc:点分十进制数串的首元素地址
dst:转换为32位无符号整数的地址
返回值:
成功返回1 、 失败返回其它
*/
例
#include
#include
int main(int argc,char *argv[])
{
char ip_str[] = "10.0.13.100";
unsigned int ip_uint = 0;
unsigned char * ip_p =NULL;//可以用char吗?
inet_pton(AF_INET,ip_str,&ip_uint);
printf("ip_uint = %d\n",ip_uint);
ip_p = (unsigned char *) &ip_uint;
printf("ip_uint = %d.%d.%d.%d\n",*ip_p,*(ip_p+1),*(ip_p+2),*(ip_p+3));
return 0;
}
整型数据转字符串格式ip地址
将32位无符号整型数据(默认大端)转成 点分十进制数组
//头文件:
#include
//len的宏定义
#define INET_ADDRSTRLEN 16 //for ipv4
#define INET6_ADDRSTRLEN 46 //for ipv6
const char *inet_ntop(int family, const void *addrptr,char *strptr, size_t len);
/*
参数:
family 协议族 AF_INET:IPv4 AF_INET6:IPv6
addrptr 32位无符号整数数据的地址
strptr 点分十进制数串的首元素地址
len 点分十进制数串的最大长度
返回值:
成功:则返回字符串的首地址
失败:返回NULL
*/
例
#include
#include
int main()
{
unsigned char ip[]={10,0,13,252};
char ip_str[16];
inet_ntop(AF_INET,(unsigned int *)ip,ip_str,16);
printf("ip_str = %s\n",ip_str);
return 0;
}
UDP协议:面向无连接的用户数据报协议,在传输数据前不需要先建立连接;目地主机的运输层收到UDP报文后,不需要给出任何确认
1、相比TCP速度稍快些
2、简单的请求/应答应用程序可以使用UDP
3、对于海量数据传输不应该使用UDP
4、广播和多播应用必须使用UDP
UDP应用: DNS(域名解析)、NFS(网络文件系统)、RTP(流媒体)等
网络通信需要解决3大问题(应用层)
1.协议
2.端口(port)
3.IP地址
20世纪80年代初,加州大学Berkeley分校在BSD(一个UNIX OS版本)系统内实现了TCP/IP协议;其网络程序编程开发接口为socket。
随着UNIX以及类UNIX操作系统的广泛应用, socket成为最流行的网络程序开发接口
socket作用:提供不同主机上的进程之间的通信
特点
1、socket也称“套接字”
2、是一种文件描述符,代表了一个通信管道的一个端点
3、类似对文件的操作一样,可以使用read、write、close等函数对socket套接字进行网络数据的收取和发送等操作
4、得到socket套接字(描述符)的方法调用socket()
- 服务端要绑定确定的端口号
- 客户端不能使用read、write,因为需要发送给指定的服务端地址。
创建一个用于网络通信的socket套接字(描述符)
//头文件:
#include
int socket(int family,int type,int protocol);
/*
参数:
family:协议族(AF_INET4、AF_INET6、PF_PACKET等)
||流式套接字 用于TCP通信 ||报式套接字 用于UDP通信 ||原始套接字
type:套接字类( SOCK_STREAM、 SOCK_DGRAM、 SOCK_RAW等)
||一般放0 自动指定协议
protocol:协议类别(0、IPPROTO_TCP、IPPROTO_UDP等)
返回值:
>0 通信的文件描述符(套接字)
<0 创建失败
*/
特点:
1.创建套接字时,系统不会分配端口
2.创建的套接字默认属性是主动的,即主动发起服务的请求;当作为服务器
3.时,往往需要修改为被动的
例
#include
#include
int main(int argc, char const *argv[])
{
//创建通信的UDP的套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
perror("socket");
exit(-1);
}
//关闭套接字
close(sockfd);
return 0;
}
注意:
AF_INET:IPv4协议
SOCK_DGRAM:数据报套接字
0:选择所给定的family和type组合的系统默认值
存放IPv4协议通信的所有地址信息
//头文件:
#include
struct in_addr
{
in_addr_t s_addr;//4字节
};
struct sockaddr_in
{
sa_family_t sin_family; //2字节 协议AF_INE4 AF_INET6
in_port_t sin_port; //2字节 端口
struct in_addr sin_addr;//4字节 IP地址(32位无符号整数)
char sin_zero[8] //8字节 全写0
};
为了使不同格式地址能被传入套接字函数,地址须要强制转换成通用套接字地址结构
//头文件:
#include
struct sockaddr
{
sa_family_t sa_family; // 2字节
char sa_data[14] //14字节
};
struct sockaddr_in //IPv4地址结构(存放客户端、服务器的地址信息(协议,port,IP))
struct sockaddr //通用地址结构,不是存放数据 socket API 类型转换
//在定义源地址和目的地址结构的时候,选用struct sockaddr_in;
//例:
struct sockaddr_in my_addr;
//当调用编程接口函数,且该函数需要传入地址结构时需要用struct sockaddr进行强制转换
//例:
bind(sockfd,(struct sockaddr*)&my_addr,sizeof(my_addr));
向to结构体指针中指定的ip,发送UDP数据
#include
ssize_t sendto(int sockfd,
const void *message,
size_t length,
int flags,
const struct sockaddr *dest_addr,
socklen_t dest_len);
/*
参数:
sockfd:从那个套接字发出
message:需要发送的消息的首元素地址
length: 消息的实际长度
flags:0 网络默认方式通信
dest_addr:指向目主机的IPv4地址信息(协议、port、IP地址)
dest_len:地址结构体的长度
返回值:
成功:发送数据的字符数
失败: -1
注意:
通过dest_addr和dest_len确定目的地址
可以发送0长度的UDP数据包
*/
#include
#include //socket
#include //struct sockaddr_in
#include //memser
#include //htons
int main()
{
//创建通信的UDP的套接字(没有port、ip)
int sockfd = socket(AF_INET, SOCK_DGRAM,0);
printf("UDP套接字sockfd=%d\n",sockfd);
//udp客户端 发送消息 给服务器
//定义一个IPv4地址结构 存放服务器的地址信息(目的主机)
struct sockaddr_in ser_addr;
memset(&ser_add, 0, sizeof(ser_add));
ser_addr.sin_family = AF_INET; //IPv4
ser_addr.sin_port = htons(8000); //服务器的端口
inet_pton(AF_INET,"10.9.21.211", &ser_addr.sin_addr.s_addr); //服务器的IP地址
//发送数据
sento(sockfd, "hello net", strlen("hello net"), 0, \
(struct sockaddr *)&ser_addr,sizeof(ser_addr));
close(sockfd);
return 0;
}
bind给udp套接字绑定固定的port、IP信息
服务器收到客户端的信息,客户端的port是随机的。如果udp套接字不使用bind函数绑定固定端口,那么在第一次调用sendto系统会自动给套接字分配一个随机端口。后续sendto调用继续使用前一次的端口。
将本地协议地址与sockfd绑定
#include
int bind(int sockfd,const struct sockaddr *address,socklen_t address_len);
/*
参数:
sockfd: socket套接字
sockaddr: 指向特定协议的地址结构指针
address_len:该地址结构的长度
返回值:
成功:返回0
失败:其他
注:只能绑定本地主机的
*/
例
#include
#include //socket
#include //struct sockaddr_in
#include //memset
#include //htos
#include //close
int main()
{
//创建通信的UDP的套接字(没有port、ip)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
printf("UDP套接字sockfd=%d\n", sockfd);
//定义IPv4地址结构,存放本机信息
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(9000);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//给udp套接字 bind绑定一个固定的地址信息
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
//udp客户端 发送消息 给服务器
//定义一个IPv4地址结构 存放服务器的地址信息(目的主机)
struct sockaddr_in ser_addr;
memset(&ser_add, 0, sizeof(ser_add));
ser_addr.sin_family = AF_INET; //IPv4
ser_addr.sin_port = htons(8000); //服务器的端口
//服务器的IP地址
inet_pton(AF_INET,"10.9.21.211", &ser_addr.sin_addr.s_addr);
//发送数据
sento(sockfd, "hello net", strlen("hello net"), 0, \
(struct sockaddr *)&ser_addr,sizeof(ser_addr));
//关闭套接字
close(sockfd);
return 0;
}
接收UDP数据,并将源地址信息保存在from指向的结构中(默认没消息,阻塞)
#include
ssize_t recvfrom(int sockfd, void *buf,size_t nbytes,int flags,
struct sockaddr *from,socklen_t *addrlen);
/*
参数:
sockfd: udp套接字
buf: 用来存放接收消息的空间起始地址
nbytes: 能接收消息的最大字节数
flags: 套接字标志(常为0)
from: 存放发送者的IPv4地址信息(不关心发送者信息,可为NULL)
addrlen: 地址结构长度
返回值:
成功:接收到的实际字节数
失败: -1
注意:
通过from和addrlen参数存放数据来源信息
from和addrlen可以为NULL, 表示不保存数据来源
*/
例
#include
#include //socket
#include //struct sockaddr_in
#include //memset
#include //htos
#include //close
int main()
{
//创建通信的UDP的套接字(没有port、ip)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
printf("UDP套接字sockfd=%d\n", sockfd);
//定义IPv4地址结构,存放本机信息
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(9000);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//给udp套接字 bind绑定一个固定的地址信息
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
//接收udp消息
while(1)
{
//定义一个IPv4地址结构,存放发送者的信息
struct sockaddr_in from_addr;
socklen_t from_len = sizeof(from_addr);
unsigned char buf[1500] = "";
int len = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&from_addr, &from_len);
//from_addr存放的就是发送者的消息
char ip[16] = "";
//转换为点分十进制ip
inet_ntop(AF_INET, &from_addr.sin_addr.s_addr, ip, 16);
printf("消息来自%s %hu--->", ip, ntohs(from_addr.sin_port));
printf("len:%d msg:%s\n", len, buf);
}
//关闭套接字
close(sockfd);
return 0;
}
同时收发数据
1、创建udp套接字socket
2、bind固定的地址信息
#include
#include
#include
#include
#include
#include //线程头文件
#include
void *send_function(void *arg)
{
//获取套接字
int sockfd = *(int *)arg;
//定义地址目的结构
struct sockaddr_in dst_addr;
bzero(&dst_addr,sizeof(dst_addr));
dst_addr.sin_family = AF_INET;
while(1)
{
//获取键盘输入
fgets(buf,sizeof(buf),stdin);
buf(strlen(buf)-1)=0;
//判断是否是IP port
//sayto IP port
if(strncmp(buf,"sayto",5) == 0)
{
char ip[16] = "";
unsigned short port = 0;
//sayto 10.9.21.211 8000
sscanf(buf,"sayto %s %hu", ip, &port);
dst_addr.sin_port = htons(port);
inet_pton(AF_INET, ip, &dst_addr.sin_addr.s_addr);
continue;
}
else
{
sendto(socked, buf, strlen(buf), 0,
\(struct sockaddr *)&dst_addr, sizeof(dst_addr));
if(strcmp(buf,"bye")==0)
break;
}
}
return NULL;
}
void *recv_function(void *arg)
{
int sockfd = *(int *)arg;
while(1)
{
struct sockaddr_in from_addr;
socklen_t from_len = sizeof(from_addr);
unsigned char buf[1500]="";
char ip[16]="";
int len = recvfrom(sockfd, buf,sizeof(buf), 0,\
(struct sockaddr *)&from_addr, &from_len);
printf("%s %hu:%s\n",inet_ntop(AF_INET,&from_addr.sin_addr.s_addr,ip,16),\
ntohs(from_addr.sin_port),buf);
if(strcmp(buf,"bye")==0)
break;
}
return NULL;
}
int main(int argc, char const *argv[])
{
// 判断参数 ./a.out 8000\n");
if(arfc != 2)
{
printf("./a.out 8000\n");
return 0;
}
//创建udp套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
//bind绑定固定的端口, IP
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(atoi(argv[1]); ///atoi 将字符串转为数字
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
//创建发送线程
pthread_d send_tid;
pthread_create(&send_tid, NULL, send_function, (void *)&sockfd);
//创建接收线程
pthread_d recv_tid;
pthread_create(&recv_tid, NULL, recv_function, (void *)&sockfd);
pthread_join(send_tid, NULL);
pthread_join(recv_tid, NULL);
//关闭套接字
close(sockfd);
return 0;
}
单个服务器与多个客户主机通信时减少分组流通
以下几个协议都用到广播
1、地址解析协议(ARP,寻找IP地址对应的MAC地址)
2、动态主机配置协议(DHCP,向路由器申请IP地址)
3、网络时间协议(NTP)
1、处于同一子网的所有主机都必须处理数据
2、UDP数据包会沿协议栈向上一直到UDP层
3、运行音视频等较高速率工作的应用,会带来大负载
4、局限于局域网内使用,不可用于广域网
{网络ID,主机ID}
网络ID表示由子网掩码中1覆盖的连续位
主机ID表示由子网掩码中0覆盖的连续位
定向广播地址:主机ID全1
1、例:对于192.168.220.0/24,其定向广播地址为192.168.220.255(所有主机必须无条件接收)
2、通常路由器不转发该广播
受限广播地址:255.255.255.255
路由器从不转发该广播(否则全世界的人都接收了)
单播:
广播:
描述:设置套接字有广播功能
#include
int setsockopt(int sockfd, int level,\
int optname, const void *optval,\
socklen_t optlen);
/*
参数:
sockfd:套接字
level:SOL_SOCKET
optname:SO_BROADCAST
optval:int 类型变量的地址,这个值设置为1
optlen:int类型变量的大小
成功返回0,否则返回-1
*/
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
//创建一个udp套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
//设置套接字允许广播
int yes = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &yes, sizeof(int));
//定义地址结构存放广播地址信息
struct sockaddr_in dst_addr;
bzero(&dst_addr, sizeof(dst_addr));
dst_addr.sin_family = AF_INET;
dst_addr.sin_port = htons(8000);
dst_addr.sin_addr.s_addr= inet_addr("10.9.21.255"); //广播地址
//广播
for(int i = 0; i < 5; i++)
{
sendto(sockfd, "this is broadcast", strlen("this is broadcast"), \
0, (struct sockaddr *)&dst_addr, sizeof(dst_addr));
sleep(1);
}
close(sockfd);
return 0;
}
数据的收发仅仅在同一分组中进行
1、多播地址标示一组接口
2、多播可以用于广域网使用
3、在IPv4中,多播是可选的
IPv4的D类IP地址是多播地址
十进制:224.0.0.1 ~ 239.255.255.254(里面任意一个都是多播地址)
十六进制:E0.00.00.01 ~ EF.FF.FF.FE
多播地址向以太网MAC地址的映射(MAC地址不能直接全部设为FF,需要进行过滤)
只能将自己主机的IP加入多播组
基于mac地址不完备硬件过滤
基于IP地址的完备软件过滤
int setsockopt(int sockfd, int level,int optname,
const void *optval, socklen_t optlen);
/*
sockfd: 加入或退出的套接字
*/
//成功执行返回0,否则返回-1
在IPv4因特网域(AF_INET)中,多播地址结构体用如下结构体ip_mreq表示:
struct in_addr
{
in_addr_t s_addr;
}
struct ip_mreq
{
struct in_addr imr_multiaddr; //多播组IP
struct in_addr imr_interface; //将要添加到多播组的IP
/*将imr_interface添加到imr_multiaddr中*/
};
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
//创建一个udp套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
//bind绑定固定的IP端口
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8000);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
//将sockfd加入多播组224.0.0.1
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1"); //多播组IP
mreq.imr_interface.s_addr = hton1(INADDR_ANY); //主机所有IP
setsockopt(sockfd, IPPORT_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
//测试接收多播组的消息
while(1)
{
unsigned char buf[1500] = "";
recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
printf("buf = %s \n",buf);
}
close(sockfd);
return 0;
}
1、面向连接的流式协议;可靠、出错重传、且每收到一个数据都要给出相应的确认
2、通信之前需要建立链接
3、服务器被动链接,客户端是主动链接
- socket 创建套接字
- connect 连接服务器
- send 发送请求
- recv 接收应答
- close
socket创建的套接字:没有端口、主动连接别人
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
注:TCP是SOCK_STREAM, UDP为SOCK_DGRAM
如果sockfd没有被bind绑定,第一次调用connect系统自动分配随机端口,后续使用该端口
int connect(int socket, const struct sockaddr *address, socklen_t address_len)
/*
socket: 套接字
address: 连接的服务器地址结构
address_len: 地址结构长度
*/
注:如果客户端和服务器通信,必须使用connect事先建立连接
ssize_t send(int socket, const void *buffer, size_t length, int flags)
/*
socket: 客户端套接字
buffer:发送的消息
length:消息长度
flags: 0
返回值:
成功:返回发送的字节数
失败:返回-1
*/
ssize_t recv(int socket, void *buffer, size_t length, int flags);
/*
socket: 客户端套接字
buffer: 接收的消息
length:能接收的最大长度
flags:0
返回值:
成功:成功接收的字节数
失败:-1
*/
#include
int close(int fildes);
#include
#include
#include
#include
#include
int main(int argc, char const *argv[])
{
//创建tcp套接字 SOCK_STREAN
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
//connect连接服务器(知道服务器地址信息)
struct sockaddr_in ser_addr;
bzero(&ser_addr, sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(8000);
ser_addr.sin_addr.s_addr = inet_addr("10.9.21.211");
connect(sockfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
//客户端发送请求
send(sockfd, "hello tcp", strlen("hello tcp"), 0 );
//客户端接收服务器的应答
unsigned char buf[1500] = "";
int len = recv(sockfd, buf, sizeof(buf), 0);
printf("服务器的应答:%s\n", buf);
//关闭套接字
close(sockfd);
return 0;
}
- socket 创建套接字
- bind 绑定固定的port、ip地址信息
- listen 监听套接字 创建连接队列
- accept
- send 发送请求
- recv 接收应答
- close
socket创建的套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
注:TCP是SOCK_STREAM, UDP为SOCK_DGRAM
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr)):
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8000);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)):
listen监听:等待客户端的连接到来,经过三次握手(底层自动),将客户端放入入连接队列
#include
int listen(int socket, int backlog);
/*
功能:
1. 将监听套接字由主动变被动
2. 为该套接字创建连接队列
参数:
socket:变被动的套接字
backlog:连接队列的大小
返回值:
成功:0
失败:-1
*/
#include
int accept(int sockfd,struct sockaddr *cliaddr, socklen_t *addrlen);
/*
功能:
从已连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待(阻塞)
参数:
sockfd: socket监听套接字
cliaddr: 用于存放客户端套接字地址结构
addrlen:套接字地址结构体长度的地址
返回值:
已连接套接字
*/
注:返回的是一个已连接套接字,服务器只能通过已连接套接字和客户端进行数据通信
ssize_t send(int socket, const void *buffer, size_t length, int flags)
/*
socket: 客户端套接字
buffer:发送的消息
length:消息长度
flags: 0
返回值:
成功:返回发送的字节数
失败:返回-1
*/
注:如果ssize_t>0,表示发送成功(实际发送的字节数)
如果ssize_t为-1,表示发送是失败
tcp不允许send发送0长度报文(服务端接收0长度报文则为客户端断开连接)
udp允许sendto发送0长度报文
ssize_t recv(int socket, void *buffer, size_t length, int flags);
/*
socket: 客户端套接字
buffer: 接收的消息
length:能接收的最大长度
flags:0
返回值:
成功:成功接收的字节数
失败:-1
*/
注:如果ssize_t为0,表示客户端已经断开连接
如果ssize_t>0,表示recv收到的实际字节数
如果ssize_t为-1,表示recv读取数据出错
#include
int close(int fildes);
#include
#include //socket
#include //struct sockaddr_in
#include //memset
#include //htos
#include //close
int main()
{
//创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
//bind绑定固定的port、ip地址信息
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(9000);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
//监听套接字 创捷连接队列
listen(sockfd, 10);
//提取客户端的连接
while(1)
{
//一次只能提取一个客户端
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int cfd = accept(sockfd, (struct sockaddr *)&cli_addr, &cli_len);
if(cfd < 0 ) //提取失败
{
perrer("accept\n");
break;
}
else
{
char ip[16] = "";
unsigned short port = 0;
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, ip, 16);
port = ntohs(cli_addr.sin_port);
//打印客户端的信息
printf("客户端:%s %d connected\n", ip, port);
while(1)
{
//获取客户端的请求
unsigned char buf[1500] = "";
int len = recv(cfd, buf, sizeof(buf), 0);
if(len == 0) //客户端已经关闭
{
//关闭与客户端连接的套接字
close(cfd);
break;
}
//应答客户端
send(cfd, buf, len, 0);
}
}
}
//关闭监听套接字
close(sockfd);
return 0;
}
客户端调用connect连接服务器时,底层会发生“三次握手”,握手成功,连接建立,connect解阻塞继续执行。
SYN 置1为连接报文
FIN 置1为断开报文
ACK 置1为回应报文
RST 复位
URG 紧急指针,比其他报文优先发送
PSH 推送
序号 当前报文的标号 seq
确认序号指希望对方下次发送数据的序号 ack_seq
客户端发送SYN请求,处于SYN_SENT状态完成第一次握手
服务端收到客户端SYN请求,处于SYN_RCVD状态,并发出SYN以及ACK请求,完成第二次握手
客户端收到服务端的SYN以及ACK处于ESTABISHED(连接状态),并发出ACK请求,完成第三次握手
当客户端调用close,激发低层发出FIN请求,完成第一次挥手
服务器收到客户端的FIN,立马回应ACK报文完成第二次挥手
服务器应用层调用close,激发底层发出FIN请求,完成第三次挥手
客户端收到服务器的FIN请求,发出ACK应答完成第四次挥手
为啥是四次挥手
第二次、第三次报文存在时间差异,不能组成一个报文发送
客户端调用close,为啥能收到服务器的FIN以及ACK?
客户端调用close只是处于版关闭状态(应用层不能收发,但底层可以)
#include
#include //socket
#include //struct sockddr_in
#include //inet_pton inet_addr
#include //bzero
#include //exit
#include //fork
#include //SIGCHLD
#include
void free_child(int sig)
{
//回收子进程的资源
while(1)
{
pid_t pid = waitpid(-1, NULL, WNOHANG);
if(pid <= 0) //没有子进程退出
{
break;
}
else if(pid > 0)
{
printf("子进程%d退出了\n", pid);
}
}
}
int main(int argc, char const *argv[])
{
if(argc != 2)
{
printf("./a.out 8000\n");
_exit(-1);
}
//1 创建tcp监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd < 0 )
{
perror("socket");
exit(-1);
}
//设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REuseADDR, &opt, sizeof(opt));
//2 bind给服务器的监听套接字绑定固定的IP、port
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(atoi(arg[1]));
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(lfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
if(ret < 0 )
{
perror("bind");
_exit(-1);
}
//3 listen将服务器的套接字主动变被动 创建连接队列 进行监听
ret = listen(lfd, 128);
if(ret < 0 )
{
perror("listen");
_exit(-1);
}
//将SIGCHLD放入阻塞集
sigset_t set;
//清空集合
sigemptyset(&set);
//将SIGCHLD放入集合中
sigaddset(&set,SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
//4 while ---> accept提取客户端
while(1)
{
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int cfd = accept(lfd,(struct sockaddr *)&cli_addr, &cli_len);
if(cfd < 0)
{
if(errno == ECONNABORTED || errno == EINTR)
continue;
break;
}
char ip[16] = "";
unsigned short port = 0;
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, ip, 16);
port = ntohs(cli_addr.sin_port);
printf("%s %hu connected\n", ip, port);
//5 一个客户端创建一个进程(父进程close(cfd),子进程close(lfd)
pid_t pid = fork();
if(pid == 0) //子进程
{
//子进程close(lfd)
close(lfd);
//6 子进程 服务客户端
while(1)
{
//获取客户端的请求
unsigned char buf[1500] = "";
int len = recv(cfd, buf, sizeof(buf), 0);
if(len <= 0)
{
printf("%s %hu 退出了\n", ip, port);
break;
}
else
{
printf("%s\n",buf);
//将数据发给客户端
send(cfd, buf, len, 0)
}
//关闭cfd
close(cfd);
_exit(-1); //进程退出 发出SIGCHLD信号
}
else if(pid > 0) //父进程
{
//父进程close(cfd)
close(cfd);
//注册SIGCHLD处理函数
signal(SIGCHLD, free_child);
//将SIGCHLD从阻塞中解除
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
默认情况下,如果一个网络应用程序的一个套接字绑定了一个端口(占用了8000),这时候,别的套接字就无法使用这个端口(8000)
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REuseADDR, &opt, sizeof(opt));
上面的socked为需要使用同一端口复用的套接字
基于TCP协议得超文本传送协议。是浏览器与服务器之间得通信协议。
一个连接只能处理一个请求。
特点:
#include
#include //socket
#include //struct sockaddr_in
#include //inet_pton inet_addr
#include //bzero
#include //_exit
#include //线程相关函数
#include
//open
#include
#include
#include
char head[] = "HTTP/1.1 200 OK\r\n" \
"Content-Type: text/htm1\r\n" \
"\r\n";
char err[] = "HTTP/1.1 404 Not Found\r\n" \
"Content-Type: text/htm1\r\n" \
"\r\n" \
"File not found";
typedef struct {
int cfd; //存放已连接套接字
struct sockaddr_in addr; //存放客户端的信息
}CLIENT_MSG;
void *deal_client_fun(void *arg)
{
CLIENT_MSG *p = (CLIENT_MSG *)arg;
//打印客户端的信息
char ip[16] = "";
unsigned short port = 0;
inet_ntop(AF_INET, &p->addr.sin_addr.s_addr, ip, 16);
port = ntohs(p->addr.sin_port);
printf("%s %hu connected\n", ip, port);
//获取浏览器的请求
unsigned char buf[1500] = "";
recv(p->cfd, buf, sizeof(buf), 0);
//提取请求中的文件名
char file_name[512] = "./htm1"; //存放本地的路劲
sscanf(buf, "GET %s", file_name+6);
printf("##%s##\n", file_name);
if(file_name[7] == '\0') //没有提取到文件名
{
strcpy(file_name,"./html/index.htm1"); //默认提出的文件
}
//从本地打开file_name文件
int fd = open(file_name, O_RDONLY);
if(fd < 0) //本地没有该文件
{
//send 404
send(p->cfd, err, strlen(err), 0);
//退出线程
close(p->cfd);
//释放堆区空间
if(p != NULL)
{
free(p);
p = NULL;
}
return NULL:
}
//本地文件打开成功
//send 200
send(p->cfd, head, strlen(head), 0);
//循环读取本地文件 发送给浏览器
while(1)
{
unsigned char file_data[1024] = "";
//读取文本文件数据
int len = read(fd, file_data, sizeof(file_data));
//将file_data发送给浏览器
send(p->cfd, file_data, len, 0);
if(len < 1024)
break;
}
//释放堆区空间
close(p->cfd);
if(p != NULL)
{
free(p);
p = NULL;
}
//线程结束
pthread_exit(NULL);
return NULL:
}
//1. 创建tcp监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd < 0)
{
perror("socket")
_exit(-1);
}
//设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//2. bind给服务器的监听套接字绑定固定的IP、port
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(atoi(argv[1]));
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(lfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
if(ret < 0)
{
perror("bind");
_exit(-1);
}
//3. listen将服务器的套接字主动变被动 创建连接队列 进行监听
ret = listen(lfd, 128)
if(ret < 0)
{
perror("listen");
_exit(-1);
}
//4. while-->accept提取客户端
while(1)
{
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int cfd = accept(lfd, (struct sockaddr *)&cli_addr, &cli_len);
CLIENT_MSG *p = (CLIENT_MSG *)calloc(1, sizeof(CLIENT_MSG));
p->cfd = cfd;
p->addr = cli_addr;
//5. 一个客户端创建一个线程
pthread_t tid;
pthread_create(&tid, NULL, deal_client_fun, (void *)p);
//线程分离
pthread_detach(tid);
}
//关闭监听套接字
close(lfd);
return 0;
}
通过对TCP、UDP的编程学习,能够完成对实际项目需求中网络功能的开发,为了提高程序的稳定性以及效率等等,通常会使用多线程、多进程开发;根据功能需求的不同,可以利用C/S、B/S模式进行开发
- ping基于ICMP协议。
- 协议栈有一张ARP表记录通信过的IP对应的mac。
- ping通过ICMP协议数据到达链路层,先去ARP表中查看目的IP对应的mac地址,如果有记录,将目的mac放入mac报文中发出去。如果arp表中没有记录,系统自动调用arp协议进行广播获取目的主机的mac,目的主机收到arp请求,单播应答。发送方就会更新arp表并将mac放入报文发送出去。
集线器工作在物理层。
数据到达集线器会被集线器广播到与集线器相连的所有设备上。
所有连接到集线器上的设备都是共享集线器的带宽。
整形放大的功能。
交换机工作在链路层(核心层),三层交换机(核心层在链路层,只是具备VLAN虚拟局域网的划分)。
交换机上的设备是单播通信。
交换机上所有设备独享带宽。
路由器是不同网段通信的桥梁。
1、如果目的IP和发送主机的IP不在同一个局域网,发送主机的网络层思考应该将数据发送给网关。
2、假如每台主机已经配置网关信息,数据传递到链路(封装mac地址),去arp表中查找网关的mac地址(如果没有,需要arp广播得到网关mac),src_mac为主机mac、dest_mac为网关的mac,发送出去数据就到达网关。
3、路由器收到数据:查看报文中的目的IP地址和当前路由器的哪个接口的IP是同一个网段。
1. 找到同一网段的接口,那么数据就会从该接口发送至目的主机(src_mac为接口的mac,dst_mas为主机的mac)就会从该接口发送至目的主机
2. 没有找到同一个网段接口,从路由器的“路由表”中查看“下一跳”,并确定当前路由器的那个接口和下一跳相连。将数据(src_mac为接口的mac,dst_mac为下一跳的mac)发送到下一跳,下一跳收到数据,重复上一个路由器的所有动作。
数据从应用层到传输层,封装源端口、目的端口,数据到达网络层封装原IP、目的IP,并查看目的IP是不是当前局域网,不是当前局域网,数据交给网关,数据传递到链路层,链路层在arp表中查找网关的mac,如果有,修改原mac为主机mac, 目的mac为网关mac,数据发送到网关,如果arp表中没有记录网关的mac地址,将调用arp协议广播得到网关的mac地址,并更新arp表,并修改原mac为主机mac,目的mac为网关的mac,数据从主机的网卡发送到网关,网关的路由器收到数据后,先进行分析数据包中的目的网段是否和路由器的某一个接口的网段是同一个网段,如果是,数据就从该接口发送到目的主机,原mac为接口的mac,目的mac为主机的mac,如果报文中的目的IP,与路由器中的所有接口都不在同一网段,这时候路由器会查看路由表,查看下一跳,并确定路由器的哪一个接口和下一跳相连,修改原mac为接口的mac,目的mac为下一跳的mac,数据发送到下一跳,下一跳重复上一跳的所有动作传递到目的主机,
原始套接字(SOCK_RAW)
1、一种不同于SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)的套接字,它实现于系统核心
2、可以接收本机网卡上所有的数据帧(数据包),对于监听网络流量和分析网络数据很有作用
3、开发人员可发送自己组装的数据包到网络上
4、广泛应用于高级网络编程
5、网络专家、黑客通常会用此来编写奇特的网络程序
TCP协议的数据
UDP协议的数据
- 内核没有处理的数据包,因此要访问其他协议
- 发送的数据需要使用,原始套接字(SOCK_RAW)
在链路层可以发送自定义的帧数据,也可以进行数据的分析、伪装等。
PF_PACKET:链路层编程
SOCK_RAW:原始套接字
ETH_P_ALL:收发 所有帧数据
#include
#include
int socket(PF_PACKET, SOCK_RAW, protocol)
/*
功能:
创建链路层的原始套接字
参数:
protocol:指定可以接收或发送的数据包类型
ETH_P_IP:IPV4数据包
ETH_P_ARP:ARP数据包
ETH_P_ALL:任何协议类型的数据包
返回值:
成功(>0):链路层套接字
失败(<0):出错
*/
在TCP/IP协议栈中的每一层为了能够正确解析出上层的数据包,从而使用一些”协议类型“来标记,如图
原始套接字必须sudo运行
1、创建原始套接字
2、recvfrom接收数据
#include
#include
#include
#include
int mian(int argc, char const *argv[])
{
int sockfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
printf("sockfd = %d\n",sockfd);
//接收数据
while(1)
{
unsigned char buf[1500] = "";
int len = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
printf("len = %d\n", len);
//分析mac报文
char src_mac[18] = ""; //源mac
char dst_mac[18] = ""; //目的mac
sprintf(dst_mac,"%02x:%02x:%02x:%02x:%02x:%02x",\
buf[0],buf[1],buf[2],buf[3],buf[4],buf[5]);
sprintf(src_mac,"%02x:%02x:%02x:%02x:%02x:%02x",\
buf[0+6],buf[1+6],buf[2+6],buf[3+6],buf[4+6],buf[5+6]);
unsigned short mac_type = 0;
mac_type = ntohs(*(unsigned short *)(buf + 12));
printf("%s-->%s:",src_mac,dst_mac);
if(mac_type == 0x0800)
{
printf("IP报文\n");
unsigned char *ip_p = buf+14;
int ip_head_len = ((*ip_p)&0x0f)*4; //从第一个字节里取ip长度
char src_ip[16] = "";
char dst_ip[16] = "";
inet_ntop(AF_INET, (void *)ip_p+12, src_ip, 16);
inet_ntop(AF_INET, (void *)ip_p+12, dst_ip, 16);
printf("\t%s--->%s:", stc_ip, dst_ip);
char ip_type = *(ip_p+9);
if(ip_type == 1)
{
printf("ICMP报文"\n");
}
else if(ip_type == 2)
{
printf("IGMP报文"\n");
}
else if(ip_type == 6)
{
printf("TCP报文\n");
unsigned char *tcp_p = buf+14+ip_head_len;
printf("\t%hu--->%hu:", ntohs(*(unsigned short *)tcp_p),\
ntohs(*(unsigned short *)(tcp_p+2)));
int tcp_head_len = ((*(tcp_p +12))>>4)*4;
//应用数据
printf("%s\n", tcp_p+tcp_head_len);
}
else if(ip_type == 17)
{
printf("UDP报文\n");
unsigned char *tcp_p = buf+14+ip_head_len;
printf("\t%hu--->%hu:", ntohs(*(unsigned short *)udp_p),\
ntohs(*(unsigned short *)(udp_p+2)));
//应用数据
printf("%s\n", udp_p+8);
}
else{
printf("未知报文\n");
}
}
else if(mac_type == 0x0806)
{
printf("ARP报文\n");
}
else if(mac_type == 0x8035)
{
printf("RARP报文\n");
}
else
{
printf("其他报文\n");
}
}
close(sockfd);
return 0;
}
广播请求,对方单播应答。
#include
#include //socket
#include //ETH_P_ALL
#include //ioctl
#include //struct ifreq
#include //strncpy
#include //struct sockaddr_ll
#include //inet_ntop
int Sendto(int sockfd, unsigned char *msg, int len, char *name)
{
//获取网络接口类型
struct ifreq ethreq;
strncpy(ethreq.ifr_name, name, IFNAMSIZ);
ioctl(sockfd, SIOCGIFINDEX, ðreq);
//定义一个网络接口变量
struct sockaddr_ll sll;
bzero(&sll, sizeof(sll));
sll.sll_ifindex = ethreq.ifr_ifindex;
len = sendto(sockfd, msg, len, 0, (struct sockaddr *)&sll, sizeof(sll));
return len;
}
int main(int argc, char const *argv[])
{
//创建原始套接字
int sockfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
//组包
unsigned char msg[1500] = {
/*--------------以太网mac头部---------14字节--------*/
0xff,0xff,0xff,0xff,0xff,0xff, /*目的mac地址*/
0x00,0x0c,0x29,0x6e,0x18,0x47, /*源mac地址,既本地mac地址*/
0x08,0x06, /*arp报文*/
/*--------------arp报文---------------28字节---*/
0x00,0x01, /*硬件类型*/
0x08,0x00, /*协议类型*/
6, /*硬件地址长度*/
4, /*协议地址长度*/
0x00,0x01, /*arp选项1表示请求*/
0x00,0x0c,0x29,0x6e,0x18,0x47, /*源mac地址,既本地mac地址*/
10,9,21,201,/源IP*/
0x00,0x00,0x00,0x00,0x00,0x00, /*目的mac地址*/
10,9,21,244 /*目的mac地址*/
}
int len = 42;
//发送报文
Sendto(sockfd, msg, len, "eth0");
//接收arp应答
while(1)
{
unsigned char buf[1500] = "";
int len = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
//判断是否是arp应答
unsigned short op = ntohs(*(unsigned short *)(buf + 20));
if(op != 2)
{
continue;
}
else //arp应答到来
{
char src_mac[18] = "";
sprintf(src_mac,"%02x:%02x:%02x:%02x:%02x:%02x:",\
buf[22+0],buf[22+1],buf[22+2],buf[22+3],buf[22+4],buf[22+5]);
char src_ip[16] = "";
inet_ntop(AF_INET, (void *)buf+28,src_ip,16);
printf("%s----->%s\n",src_ip,src_mac);
break;
}
}
close(sockfd);
return 0;
}