基本知识:
1.协议
【作用】:使交换信息的两个部分之间相互理解的一组规则、
约定和数据结构的集合。
即:为了使不同版本的计算机能相互沟通而存在
在现在的网络应用中使用最广泛的协议是TCP/IP协议
ISO
OSI【【七层协议】】模型
【1】.应用层:为应用(程序提供服务)并(规定)应用程序中(通信的相关细节),
如:ftp,tftp,smtp,ssh,telnet...
【2】.表示层:主要负责数据格式的转换(设备固有格式 <==> 网络格式)
【3】.会话层:主要建立和断开通信连接
【4】.传输层:起着可靠传输的作用
【5】.网络层:将数据传输到目标地址,主要负责寻址和路由选择
【6】.数据链路层:负责物理层面上的互连的节点之间的通信传输
【7】.物理层:负责0、1比特流与电压的高低、光的闪灭之间的互换
根据七层协议模型, tcp/IP协议可分为四层
【1】.应用层:
【2】.传输层: TCP UDP
【3】.网络层: IPv4 IPv6
【4】.网络接口层
2.常用协议
【1】.TCP协议
传输控制协议,是一种【面向连接】的协议,类似打电话
【2】.UDP协议
用户数据报协议,是一种【无连接】的协议,类似发短信
【3】.IP协议
互联网协议,是上述两种协议的底层协议,当需要【开发
新的通信协议】时,才需要关注
3.IP地址
【作用】:作为通信设备在互联网中的唯一地址标识
【本质】: 1.由32位二进制组成的【整数】(IPv4)
2.由128位二进制组成的【整数】(IPv6)
日常生活中采用点分十进制表示法来描述IP地址,也就是将每个字节的
二进制转为十进制的整数,不同的整数之间用小数点分隔
0x01020304 ==>1.2.3.4
2^32个地址,怎么管理?
将【IP地址分为两部分】:网络地址和主机地址
【网络地址】:属于哪个网络 【可以定位在哪个网吧上网】
【主机地址】:网络中主机的编号 【可以定位在哪台电脑上网】
根据网络地址和主机地址位数的不同分为四类:
A: 0 + 7位的网络号 + 24位主机地址
0.0.0.0 ~ 127.255.255.255
B: 10 + 14位网络号 + 16位主机地址
128.0.0.0 ~ 191.255.255.255
C: 110 + 21位网络号 + 8位主机地址
192.0.0.0 ~ 223.255.255.255
D: 1110 + 28位多(组)播地址
224.0.0.0 ~ 239.255.255.255
E: 备用
查看IP地址的命令
ipconfig
ifconfig
3.子网掩码 【第一种分IP地址的方法】
【作用】:用于划分IP地址中的网络地址和主机地址,
也可以用于判断两个IP是否在同一局域网中
具体分法: 【IP地址】 跟 【子网掩码】 做【与运算】 得到 【网络地址】
【前提是换成二进制】
IP地址 & 子网掩码 = 网络地址
例:
172.30.100.64 IP
& 255.255.255.0 子网掩码
--------------------------------
172.30.100.0 ---网络地址
64---主机地址
例:
IP:166.111.160.1 和 166.111.161.45
子网掩码:255.255.254.0
解析:
166.111.160.1
255.255.254.0 &
---------------
166.111.160 网络号
166.111.161.45
255.255.254.0 &
---------------
166.111.160 网络号
总结:上面两个IP地址在同一个局域网中
【第二种分IP地址的方法】:
斜杠'/'后面的数字N表示前N位为网络号
166.111.160.1 /23
166.111.161.45 /23
23:表示前23位为网络号
4.端口号
【IP地址】 -- 定位到具体的某一台主机/设备 【推断出你在哪个网吧哪台机子上网】
【端口号】 -- 定位到主机/设备上的某一个进程 【推断出你在那台机子玩什么】
本质上就是一个16位的无符号整数 unsigned short,范围是0~65535
其中0-1024之间的端口号被系统使用,建议【从5000开始使用】
【网络编程】中需【要提供】两个信息:【IP地址】+【端口号】
5.字节序(多字节整数)
小端模式:主要指将低位字节数据保存在低位内存地址的系统
大端模式:主要指将低位字节数据保存在高位内存地址的系统
【在准备通信地址和使用通信地址信息时需要注意类型的转换】
----------------------------------------------------------
网络编程【又名socket编程,因为要使用socket函数得到信息载体】
一、网络编程又叫socket编程
【套接字概念】:是一个网络编程的接口,是网络数据传输的软设备。
【套接字作用】:用于网络交互。
【网络编程本质】:编写程序【使两台连网的计算机】相互【交换数据】。
unix/linux系统作为服务器操作系统存在至今,因此,
Linux的网络功能应该是非常全面和强大。
网络编程其实有很成熟的通信模型,并且windows也通用。
二、通信模型(基于TCP的一对一的通信模型)
【1】.一对一服务器:
【等待连接原理】:
1 有人从很远很远的地方尝试调用 connect()来连接你的机器上的某个端口
(当然是你已经在 listen()的)。
2 他的连接将被 listen 加入等待队列等待 accept()函数的调用(加入等待队
列的最多数目由调用 listen()函数的第二个参数 backlog 来决定)。
3 你调用 accept()函数,告诉他你准备连接。
4 accept()函数将返回一个新的套接字描述符,这个描述符就代表了这个连接!
好,这时候你有了【两个套接字描述符】,返回给你的那个就是和远程计算机的连接,
而第一个套接字描述符仍然在你的机器上原来的那个端口上 listen()。
这时候你所得到的那个新的套接字描述符就可以进行 send()操作和recv()操作了。
http://blog.csdn.net/wukui1008/article/details/7669173 【套接字】
【监听套接字】:创建通信载体时得到的返回值;
作用:用来监听一个端口,当有客户端过来时,给他指路
【连接套接字】:等待客户端连接完成后得到的返回值;
作用:作为和客户端(远程计算机)真正联系的【链接】
监听套接字就是个牵线指路的,你实质上是跟它指的那个人说话。
因为你要找的那个人不可能随时等你来,而监听套接字就是专职等你来问,
它回答你要找的人在哪,并唤醒你要找的人,于是通话就建立起来了,
就像现实生活中的接线员一样。
1.创建监听套接字 使用socket()函数
原型:int socket(int domain, int type, int protocol);
即:socket(协议族,通信类型,具体的协议);
功能:
主要用于创建可以实现通信的交流点,也就是socket通信载体(相当于电话机)
得到监听套接字
返回值:
成功:返回新的socket描述符【监听套接字】
失败:返回-1,errno被设置
例如:
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
AF_INET:域/协议族,决定了是【本地通信还是网络通信】
AF_UNIX/AF_LOCAL 用于实现本地通信
AF_INET 用于实现基于IPv4的网络通信
AF_INET6 用于实现基于IPv6的网络通信
SOCK_DGRAM:通信类型,决定了【具体的通信式】
SOCK_STREAM 提供有序的、可靠的、双向的、面向连接的字节流通信方式,默认使用TCP协议
SOCK_DGRAM 提供无序的、不可靠的,非面向连接的数据报通信方式,默认使用UDP协议
0:指定具体的协议,默认为0,使用默认协议
2.准备通信地址【要先清零再赋值】
通信地址数据【类型】:
1】.通用地址结构
struct sockaddr
{
sa_family_t sa_family; //域/协议族
char sa_data[14]; //??????
};
2】.IPv4通信地址结构
struct sockaddr_in
{
sa_family_t sin_family;//协议族,AF_INET
in_port_t sin_port;//16位的端口号
struct in_addr sin_addr;//IP地址
};
struct in_addr
{
in_addr_t s_addr; //16位的端口号
};
例如:(IPv4通信地址结构)
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr(argv[1]);
【作用】:(以IPv4通信地址结构为例)
saddr.sin_family:决定该地址遵循何种协议
saddr.sin_port:决定该地址的端口 【哪台机子】
saddr.sin_addr.s_addr:决定该地址的IP地址 【哪个网吧】
【IP地址格式转换】:
IP地址从main()函数【输入时是字符串】格式的,
初始化通信地址的时候要用inet_addr()函数转换成结构体类型;
接收【调用】别人的IP地址时,得到的是【结构体类型】的;
若要【输出】,则要用inet_ntoa()函数转换成【字符串】格式;
【端口号格式转换】:
端口号从main()函数【输入时是字符串】格式的,
初始化通信地址的时候要先用atoi()函数转换成int型;
是主机字节顺序,要再用 htons()函数转换成网络字节顺序(大端模式);
接收【调用】别人的IP地址时,得到的是【网络字节顺序】;
若要【输出】,则要用ntohs()函数转换成【主机字节顺序】;
端口格式转换相关【函数详解】:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h:host 本地格式
n:network 网络格式(大端模式)
s:short 2字节整数
l:long 4字节整数
功能:把本地格式转成网络格式,或者反过来。
IP地址格式转换相关函数:
in_addr_t inet_addr(const char *cp);
功能:将字符串形式的IP地址转为整数类型
char *inet_ntoa(struct in_addr in);
功能:将结构类型的IP地址转为字符串形式
/*************服务器**************/
3.绑定(通信地址与监听套接字) bind()函数
原型:int bind(int sockfd, const struct sockaddr *saddr,
socklen_t addrlen);
即:bind(监听套接字描述符,强转后的通信地址,通信地址大小) ;
功能:
主要用于绑定socket和具体的通信地址
返回值:
成功:返回0
失败:返回-1,errno被设置
例如:
int ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(ret < 0) //很有必要!!
{
perror("bind");
exit(-1);
}
sockfd:socket描述符,即监听套接字
(struct sockaddr*)&saddr:结构体指针,
不管是什么协议的地址结构,都需要强转为该类型
sizeof(saddr):通信地址结构的大小,使用sizeof()计算即可
4.监听 使用listen()函数
原型:int listen(int sockfd, int backlog);
即:listen(监听套接字,监听队列大小);
功能:
为套接字sockfd建立一个连接请求监听队列,在调用listen函数成功后,这个套接字便成为服务套接字,即被动套接字
返回值:
成功:返回0
失败:返回-1,errno被设置
例如:
listen(sockfd,10);
sockfd:socket描述符,即监听套接字
10:监听队列的大小
5.等待连接 accept()函数
原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
即:accept(监听套接字,强转后的通信地址,指针类型的通信地址大小);
功能:
用于接收服务套接字sockfd上的连接请求
返回值:
成功:返回一个【连接套接字】
(新的,在后面的代码中起作用,代替监听套接字出现)
失败返回-1,errno被设置
例如:
socklen_t len = sizeof(caddr); //不能直接在等待中用sizeof(caddr)
//保存客户地址的结构体的大小,必需由调用者初始化
int confd = accept(sockfd,(struct sockaddr*)&caddr,&len);
caddr: 结构体指针,用于保存接受的客户端通信地址,
与被初始化的通信地址一起定义,用来存储客户端的地址
sockfd:socket函数的返回值,监听套接字
(struct sockaddr*)&caddr:结构体指针,
不管是什么协议的地址结构,都需要强转为该类型
&len:指针类型,用于保存客户端网络地址的信息的长度
注:如果对客户地址不感兴趣,那么第二、三两个参数写NULL即可。
如果需要客户的地址,那么第三个参数必须由调用者初始化。
6.通信
【方法】:把连接套接字当成文件描述符操作,
可以读取里面的东西,也可以往里面写东西
7.关闭连接套接字和监听套接字
close(confd);
close(sockfd);
/*********************服务器*************/
/*****************客户端*************/
3.连接 使用connect()函数
原型:int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
即:connect(监听套接字描述符,服务器的通信地址,通信地址结构的大小);
功能:
用于连接服务器
返回值:
成功:返回0
失败:返回-1,errno被设置
例如:
int ret = connect(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(ret < 0)
{
perror("connect");
exit(-1);
}
4.通信
【方法】:把监听套接字当成文件描述符操作,
可以读取里面的东西,也可以往里面写东西
5.关闭监听套接字
close(sockfd);
/****************客户端*************/
迭代服务器
并发服务器
多进程服务器
多线程服务器
多路复用服务器
【2】.IO多路复用服务器
【作用】:实现多个文件的查询是否就绪,
在某个文件可读的情况下,马上读到数据,
【方法】:运用select()函数
【原理】:对集合中的文件进行轮询,每隔一段时间就会去
询问文件描述符[0,nfds)中的每一个文件,查看是否就绪,
如果就绪那么把相应的文件描述符集合中置1. .......
直到有文件就绪或超时或出错。
超时时间结构体:
struct timeval {
time_t tv_sec; /* 秒 */
suseconds_t tv_usec; /* 毫秒 */
};
例如:
struct timeval tv;//超时时间结构体
/* 超时时间设为5秒. */
tv.tv_sec = 5;
tv.tv_usec = 0;
select()【函数原型】:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
即:select(感兴趣的文件集合中最大的文件描述符,
对读感兴趣的文件描述符集合,
对写感兴趣的文件描述符集合,
对异常感兴趣的文件描述符集合,
超时时间);
返回值:
1.有文件数据就绪,立马返回,返回就绪的文件描述符个数 >0
2.超时返回,返回0
3.出错了,返回-1 ,errno被设置
例如:
int retval;
retval = select(10, &tmpfds, NULL, NULL, &tv);
if(retavl < 0)
{ //出错
perror("select");
}
else if(retavl > 0)
{ //有文件就绪
。。。。
}
else if(retval == 0)
{ //等待超时
。。。。
}
【辅助函数】:
1】. void FD_ZERO(fd_set *set);
功能:初始化指定的集合,全部清零
例如:
FD_ZERO(sockfd,&rfds);
2】. void FD_SET(int fd, fd_set *set);
功能:把指定的fd设置到集合中去
例如:
FD_SET(sockfd,&rfds);
3】. int FD_ISSET(int fd, fd_set *set);
功能:判断指定的fd是否在这个集合中
返回值:
成功:返回非0
失败:返回0
例如:
FD_ISSET(sockfd,&rfds);
4】. void FD_CLR(int fd, fd_set *set);
功能:把指定fd从集合中移除
例如:
FD_CLR(sockfd,&rfds);
select使用【步骤】:
1.设置文件描述符
fd_set rfds,tmpfds;//创建两个文件描述符集合
struct timeval tv;//定义一个超时时间结构体
int retval; //定义一个描述符作为轮询函数的返回值,
//用于划分轮询结果以便分配任务
2. 初始化指定的文件集合,全部清零
FD_ZERO(&rfds);//把文件描述符集合清零,即初始化
3. 把指定的文件的文件描述符设置到集合中去
FD_SET(i, &rfds);//把文件描述符i,放到集合rfds中,即把对应的位置1
4.初始化时间结构体 ,
/* 超时时间设为5秒. */
tv.tv_sec = 5;
tv.tv_usec = 0;
5.备份需要轮询的文件描述符集合
tmpfds = rfds;//拷贝准备好的描述符集合,
//每循环一次,备份一次文件描述符,启动一次轮询函数
//防止源集合被覆盖
6.启动轮询函数
retval = select(fd_max+1,&tmpfds,NULL,NULL,&tv);
7.判断指定的fd是否在这个集合中
FD_ISSET(i,&rfds) //判断指定的文件是否在集合中
8. 把指定fd从集合中移除
FD_CLR(i,&rfds);//从集合中移除这个连接套接字描述符
9. 关闭文件
close(i);
【TCP与UDP协议的比较】:
1.tcp协议:
传输控制协议 类似打电话
面向连接的 (建立连接-->进行通信-->断开连接)
在通信的整个过程中必需保持连接
该协议保证了数据的传递是可靠且有序的
属于全双工的字节流通信方式
服务器压力较大,资源消耗大,执行效率较低
2.udp协议:
用户数据报协议
面向非连接的 类似发短信
在通信的整个过程中不需要保持连接
不保证数据的可靠性和有序性
属于全双工的数据报通信方式
服务器压力较小,资源消耗小,执行效率高
【3】.基于【UDP协议】的通信模型
【注意】:
1.UDP不分服务器和客户端
2. UDP调用接收函数必须要绑定地址,
要用socklen_t len = sizeof(saddr);
求出目标地址长度,在函数中用&len代表长度
3.调用方式函数不用绑定地址,也不要先求出长度
用法:
1.创建监听套接字
使用socket()函数
2.准备通信地址
定义并初始化结构体变量
/******************接收方**************/
3.绑定监听套接字和通信地址
使用bind()函数
4.通信
————————————————————————————
接收数据报函数:recvfrom()
原型:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
即:recvfrom(监听套接字描述符,强转的被发送的数据首地址,
发送数据的大小,发送标志,
发送数据的目标地址,目标地址的大小);
作用:
将目标地址发过来的N个字节存到缓冲区中
例如:
socklen_t len = sizeof(saddr);
ret = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&saddr,&len);
sockfd: socket函数的返回值,监听套接字
buf: 被发送的数据首地址
sizeof(buf): 发送数据的大小
0: 发送标志
0:功能与write一样,即阻塞
MSG_DONTWAIT: 非阻塞
(struct sockaddr *)&saddr:发送数据的目标地址
&len:目标地址的大小
**********************************/
/***************发送方*************/
3.通信
————————————————————————————
发送数据报函数:sendto()
原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
即:sendto(监听套接字描述符,强转的被发送的数据首地址,
发送数据的大小,发送标志,
发送数据的目标地址,目标地址的大小);
作用:
将准备好的数据中的N个发送给大小为M的目标地址,进行通信
例如:
int res = sendto(sockfd,str,strlen(str),0,
(struct sockaddr *)&saddr,sizeof(saddr));
sockfd: socket函数的返回值,监听套接字
str: 被发送的数据首地址
strlen(str): 发送数据的大小
0: 发送标志
0:功能与write一样,即阻塞
MSG_DONTWAIT: 非阻塞
(struct sockaddr *)&saddr:发送数据的目标地址 【发给谁】
sizeof(saddr):目标地址的大小
**********************************/
5.关闭套接字
close(sockfd);
【UDP】应用
广播 组播
发送方:1.要有一个套接字
2.要有一个目标地址(不是给自己用的,所以不用绑定)
3.要用专门的函数sendto()发送数据报
接收方:1.要有一个套接字
2.要有一个给自己用的通信地址(需要绑定)和
一个存储发送者资料的地址
3.
方法一:
使用setsockopt()函数将默认接收的缓冲区设计为一个小的缓冲区,
缓冲区地址和大小任意定义,使用getsockopt()函数得到套接字选项值
例如:
setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,(void*)&j,sizeof(j));
getsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,(void*)&i,&size);
方法二:
定义一个多播组结构体变量,并初始化,使用setsockopt()函数将默认接收
的缓冲区设计为一个小的缓冲区,缓冲区地址为定义结构体变量的首地址,
大小为结构体变量所占字节数
例如://加入多播组的方式
struct ip_mreq join_addr;
memset(&join_addr,0,sizeof(join_addr));
join_addr.imr_multiaddr.s_addr = inet_addr(argv[1]);// 组播组的IP地址。
join_addr.imr_interface.s_addr = htonl(INADDR_ANY); // 本地某一网络设备接口的IP地址。
setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,(void*)&join_addr,sizeof(join_addr));
4.绑定监听套接字和通信地址
5.使用专门的函数recvfrom()接收数据报
http://blog.csdn.net/chary8088/article/details/2486377
【套接字】:
【概念】:通信的两方的一种约定,用套接字中的相关函数来完成通信过程
【套接字选项】:
【概念】:每个【套接字】在不同的协议层次上【有】不同的【行为属性】,
那么这个【行为属性】就称为套接字选项
【作用】: 设置地址复用
允许发送广播消息
将主机加入多播组
设置发送与接收缓冲区的大小等
我们正常使用套接字编程时,一般只关注数据通信,而忽略套接字的不同特性
但有时需要设置地址复用,允许发送广播消息,将主机加入多播组,设置发送与接收缓冲区的大小等
这些都需要对套接字选项进行设置。
【所用函数】:
原型:int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
即:getsockopt(监听套接字,套接字选项所在的协议层次,
套接字选项的名称,缓冲区首地址,
缓冲区的长度);
功能: 得到套接字选项值
返回值:
成功:返回0。
失败:返回-1,errno被设置
例如:
原型:int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
即:setsockopt(监听套接字,套接字选项所在的协议层次,
套接字选项的名称,缓冲区首地址,
缓冲区的长度);
功能:如果认为套接口的默认发送以及接收缓冲区的尺寸太大时,
作为程序设计者的我们可以【将默认发送的缓冲区设计为一个小的缓冲区】。
将socket加入一个组播组,因为socket要接收组播地址224.0.0.1的
数据,它就必须加入该组播组。结构体struct ip_mreq mreq是
该操作的参数,下面是其定义:
struct ip_mreq
{
struct in_addr imr_multiaddr; // 组播组的IP地址。
struct in_addr imr_interface; // 本地某一网络设备接口的IP地址。
};
一台主机上可能有多块网卡,接入多个不同的子网,
imr_interface参数就是指定一个特定的设备接口,
告诉协议栈只想在这个设备所在的子网中加入某个组播组。
有了这两个参数,协议栈就能知道:在哪个网络设备接口上
加入哪个组播组。为了简单起见,我们的程序中直接写明了
IP地址:在172.16.48.2所在的设备接口上加入组播组224.0.1.1。
这个操作是在网络层上的一个选项,所以级别是SOL_IP
IP_ADD_MEMBERSHIP选项把用户传入的参数拷贝成了
struct ip_mreqn结构体: