一网络预备知识
1.IP 主机的标识,32bit 无符号的二进制,通常用点分十进制表示
3个基本类:
A 类:最高字节高位0 1 + 3 网络号 + 主机号
0.0.0.0191.255.255.255 2^162
192.0.0.0239.255.255.255
用途:常用作组播地址
E 类:最高字节高位1111 0
240.0.0.065535
150000 //系统用的
5000124]; //填充字段
本地地址结构体 struct sockaddr_un {
sa_family_t sun_family; // 协议族
char sun_path[108];
//108字节协议地址
};// 传参
void * arg ;
通用地址结构体:
struct sockaddr {
sa_family_t sa_family; // 协议族
char sa_data[14]; //14字节协议地址
};
一创建套接字
Int socket(int domain, int type, int protocol); 功能
domain:指明所使用的协议族,通常为PF_INET/AF_INET,表示互联网协议族(TCP/IP协议族);
type:指定socket的类型:SOCK_STREAM (TCP)或SOCK_DGRAM(UDP) protocol:协议的编号通常赋值\"0\" 返回值
Socket()调用返回一个整型socket描述符,你可以在后面的调用使用它。
失败返回 -1 Socket接口还定义了原始Socket(SOCK_RAW),允许程序使用低层协议。 绑定端口
Int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能:把sockfd 绑定一个具体的端口 sockfd : 描述符
addr : 本机的结构ip的地址不允许绑定非本机IP 如:(192.168.2.10) addrlen: 告知内核ip地址大小, 必须为实际的地址大小网络为16 正确返回0 失败返回-1
udp接收数据端
Ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen);) 功能接收数据 Sockfd 描述符
Buf 存放接收到的数据
Len 最多可以接收的数据大小
Flags 接收的方式(默认是阻塞,通常是0) Src_addr 获取发送端的ip地址信息
Addrlen 告知内核发送端ip地址大小(结构大小) 返回值
成功返回实际接收的数据大小
返回值如果为0
表示对方已关闭 失败返回 -1
udp发送数据端
Ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,struct sockaddr *dest_addr,socklen_t addrlen); 功能:发送数据 Sockfd : 描述符
Buf: 用户需要发送的数据缓存地址 Len:用户最多发送的数据大小
Flags:发送方式(默认是阻塞,通常设为0) Dest_addr: 当前数据发送的目标主机ip地址值
Addrlen: 告知目标主机ip地址的大小(结构大小) 返回值
成功返回实际发送的数据大小 失败返回 – 1
Tcp申请三次握手(客户端)
Int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 实现客户端与服务器的连接 Sockfd: 套接字描述符 Addr:服务器ip结构地址
Addrlen:服务器ip结构地址大小 成功返回 0
失败返回-1 注意:再次申请,它会断开先前建立的连接,重新建立新的连接连接:获知对方存在
(Tcp)监听(服务器端)
Int listen(int sockfd, int backlog); 功能:实现对客户端请求的监听(队列机制) Sockfd: 套接字描述符
Backlog:队列的长度,一般设为5 追打可设为8 返回值:
成功返回 0,失败返回 -1
(Tcp)回复三次握手(服务器端) 功能:回复客户端握手申请,建立连接
Int accept(int sockfd,struct sockaddr *addr, socklen_t *addrlen); Sockfd: 套接字描述符
Addr: 对方(客户端)ip 地址
Addrlen:对方(客户端)ip 地址大小
成功返回非负整数(新的socket描述符)失败返回
-1
Tcp 数据接收
Ssize_t recv(int sockfd,void *buf,size_t len,int flags); 功能接收数据
Sockfd: (客户端)是套接字描述符(服务器端)是accept()的返回值 Buf: 存放接收到的数据
Len: 最多可以接收的数据大小
Fags: 发送方式(默认是阻塞,通常设为0) 成功返回实际接收的数据大小失败返回 -1; 返回值如果为0
表示对方已关闭
Tcp数据发送
Ssize_t send(int sockfd,void *buf,size_t len,int flags); 功能:发送数据
Sockfd : (客户端)是套接字描述符(服务器端)是accept()的返回值 Buf: 用户需要发送的数据缓存地址 Len:用户最多发送的数据大小
Flags:发送方式(默认是阻塞,通常设为0) 成功返回实际发送的数据大小失败返回 -1 关闭套接字 @1
Int Shutdown(int sockfd,int how) 功能:指定方式关闭套接字 Socket: 套接字描述符
How: SHUT_RD 或0 (关闭读) SHUT_WR或1(关闭写) SHUT_RDWR或2(关闭读写)相当于close() 成功返回
0 失败返回 -1 @2
Int close(int sockfd) 关闭套接字
Socket: 套接字描述符
成功返回
0 失败返回 -1
UDP 客户端创建流程
1 初始化网络地址结构体(服务器端)
struct sockaddr_in ser_addr ;
bzero(&ser_addr,sizeof(ser_addr)); ser_addr.sin_family = AF_INET; ser_addr.sin_port = htons(50001); 服务器端口号 -------网络字节序
ser_addr.sin_addr.s_addr = inet_addr(\"192.168.1.230\") ; 服务器点分制地址-》网络字节序 2 创建数据报套接字
Int sockfd = socket(AF_INET,SOCK_DGRAM,0);(数据报套接字) 3 发送数据到服务器端 Int sento_udp = sendto(sockfd,buf,strlen(buf)+1,0,(struct sockaddr *)&ser_addr,sizeof(struct sockaddr_in));
(服务器地址) 4 接收服务器端的回复
Int recvfrom_udp = recvfrom(sockfd,buf, sizeof(buf),0,NULL,NULL); 5 关闭套接字
Close(sockfd);
UDP 服务器端创建流程
1 初始化网络地址结构体(服务器端)
struct sockaddr_in ser_addr ;
bzero(&ser_addr,sizeof(ser_addr)); ser_addr.sin_family = AF_INET; ser_addr.sin_port = htons(50001); 服务器端口号 -------网络字节序
ser_addr.sin_addr.s_addr = inet_addr(\"192.168.1.230\") ; 服务器点分制地址-》网络字节序 2 创建数据报套接字
Int sockfd = socket(AF_INET,SOCK_DGRAM,0);(数据报套接字) 3 绑定套接字
Int bind_udp = bind(sockfd,(struct sockaddr *)&ser_addr,sizeof(struct sockaddr_in));
(服务器地址) 4 接收客户端请求
struct sockaddr_in client_addr ; size = sizeof(struct sockaddr_in) int recvfrom_udp = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&client_addr,&size); (客户端地址) 5
回复客户端
Int sendto_udp =
sendto(sockfd,buf,recvfrom_udp,0,(struct sockaddr *)&client_addr, sizeof(struct sockaddr_in))
(客户端地址) 6 关闭套接字
Close(sockfd);
TCP 客户端创建流程
1 初始化网络地址结构体(服务器端)
struct sockaddr_in ser_addr ;
bzero(&ser_addr,sizeof(ser_addr)); ser_addr.sin_family = AF_INET; ser_addr.sin_port = htons(50001); 服务器端口号 -------网络字节序
ser_addr.sin_addr.s_addr = inet_addr(\"192.168.1.230\") ; 服务器点分制地址-》网络字节序 2 创建流式套接字
Int sockfd = socket(AF_INET,SOCK_STREAM,0);(流式套接字) 3
申请三次握手
Int connect_tcp = connect (sockfd,( struct sockaddr *)&ser_addr,sizeof(ser_addr))
(服务器地址) 4 发送数据到服务器端
Int send_tcp = send(sockfd,buf,strlen(buf) + 1,0) 5 接收服务器端的回复
Int recv_tcp = recv (sockfd,buf,sizeof(buf),0) 6 关闭套接字
Close(sockfd);
TCP 服务器端创建流程
1 初始化网络地址结构体(服务器端)
struct sockaddr_in ser_addr ;
bzero(&ser_addr,sizeof(ser_addr)); ser_addr.sin_family = AF_INET; ser_addr.sin_port = htons(50001); 服务器端口号 -------网络字节序
ser_addr.sin_addr.s_addr = inet_addr(\"192.168.1.230\") ; 服务器点分制地址-》网络字节序 2 创建数据报套接字
Int sockfd = socket(AF_INET,SOCK_DGRAM,0);(流式套接字) 3 绑定套接字
Int bind_tcp = bind(sockfd,(struct sockaddr *)&ser_addr,sizeof(struct sockaddr_in));
(服务器地址) 4
监听客户端请求
Int listen_tcp = listen(sockfd,5)
一般为5 最大为8 5 回复客户端的三次握手请求
struct sockaddr_in client_addr ; int len = sizeof(struct sockaddr_in);
Int connectfd = accept(sockfd,( struct sockaddr *)&client_addr,&len)
( 客户端地址) 6 接收客户端请求 7 8 Int recv_tcp = recv(connectfd,buf,sizeof(buf),0); 回复客户端
Int send_tcp = send(connectfd,buf,recv_tcp,0); 关闭套接字
Close(sockfd);
Close(connectfd); 1 .TCP CS 模型
client:
socket //创建流式套接字
|
ser_addr:(struct sockaddr_in) //目标地址结构体(服务器)
|
connect()//3次握手
|
sendto/send/write
|
recvfrom/recv/read
|
.......
|
close()/shutdown()
server:
(1)循环服务器,可以多个客户端服务,但是不能在同一时刻
ser_addr :(struct sockaddr_in)//本机的地址结构体
|
socket (流式套接字)
|
bind(使套接字具有地址属性)
|
listen(创建监听队列)
| while(1) {
accept(握手建立连接,获取对方地址) | while(1) {
recvfrom/recv/read //接收对端(客户端)信息
|
sendto/send/write //向对端回射信息
} |
.....} close
(2) 并发服务器:可以同一时刻为多个客户端服务
ser_addr :(struct sockaddr_in)//本机的地址结构体
|
socket (流式套接字,sockfd)
|
bind(使套接字具有地址属性)
|
listen(创建监听队列)
| while(1) { connectfd = accept(握手建立连接,获取对方地址)
|
pid = fork()
if(pid == 0)
{
close(sockfd);
while(1)
{
recvfrom/recv/read //接收对端(客户端)信息
|
sendto/send/write //向对端回射信息
}
} close(connectfd); | ..... } close(sockfd);
2.分析三次握手
client
server
SYN = 1 (请求标志)
seq_no = 0(client)
第一次
------------------------------>
SYN = 1(请求标志)
ACK = 1(应答标志)
seq_no = 0(server), ack_no = 1 (==seq_no(client) + 1) 第二次
ACK = 1(应答标志)
seq_no = 1(ack_no(server)),ack_no = 1 ( == seq_no(server) + 1) 第三次 ------------------------------>
3.数据包分析
一帧数据(TCP) = mac头 + IP头 + TCP头 + 用户数据
ttl: 数据包每经过一个路由器,如果停留的时间小于1s,ttl 减一,当ttl 小于0时数据丢弃掉
第三天
fcntl
int fcntl(int fd, int cmd, .../* arg */ ); 获取或改变文件描述符的属性,一般我们需要改变文件状态标志位 @1 fd : 文件描述符 @2 cmd : 对文件描述符的操作(一般可以获取或者设置当前 file status flags) (F_GETFD,F_SETFD) @3 ......: 不定参,取决于cmd @4 成功返回值取决于 cmd
失败返回-1 例如 @1
int flag = fcntl(0,F_GETFL)
查看属性有返回值 @2 fcntl(0,F_SETFL,flag | O_NONBLOCK)
添加属性无返回值
IO的特性与接口没有关系,与描述符属性有关,调用fcntl/ioctl (可以直接把你用户的命令传递到内核,可以实现对底层驱动的控制)
一 IO 模型 (4种)
1.阻塞IO
当资源未准备好时,程序睡眠或者等待,不浪费CPU,效率低,实现非常简单,但是它是应用最广泛的IO
read(sockfd,buf,BUFF_SIZE )/recv/recvfrom
2.非阻塞IO
当资源未准备好时,直接返回错误码(errno),不断的轮循,浪费CPU,效率高
3.信号驱动IO(SIGIO)
一种异步的通信机制,底层(内核)向上层(用户层)发信号(SIGIO),当资源可用时,内核向当前进程发送SIGIO信号,用户捕捉(signal)此信号,读取IO资源,如果用户不捕捉,进程会被杀死
signal(SIGIO,hander);//更安全 获取套接字的原有属性
int flag = fcntl(sockfd,F_GETFL);//O_NONBLOCK 添加异步属性,文件描述符可以被多个进程打开,此时内核不知信号发给谁 fcntl(sockfd,F_SETFL,flag | O_ASYNC); 获取当前描述符对应的默认进程号(默认为0) pid = fcntl(sockfd,F_GETOWN ); 改变描述符对应的进程号
fcntl(sockfd,F_SETOWN,getpid());
4 .IO 多路复用
可以同时对多个IO控制,哪个准备好了,执行哪个
IO 多路复用:
1.建立一个统计表:
fd_set readfds; 2.添加fd到表中
FD_ZERO(&readfds); //将表清空
FD_SET( fd,&readfds); //将fd 加入 readfds 3 .监测readfds 这张表(监测已经加入表的fd),会将没有就绪的fd 清0 n = select(fd+1,&readfds,NULL,NULL,NULL); //n ==>有多少fd 就绪,此时不知道哪个就绪 4.判断哪个fd 就绪,这张表只会保存就绪的fd
FD_ISSET(fd,&readfds) == 1
就绪
FD_ISSET(fd,&readfds) == 0
未就绪
相关函数
void FD_CLR(int fd, fd_set *set); int FD_ISSET(int fd, fd_set *set); void FD_SET(int fd, fd_set *set); void FD_ZERO(fd_set *set);
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 注意:描述符不受限与套接字,任何描述符都行
nfds:select()函数监视的描述符数的最大值,一般取监视的描述符数的最大值+1, 其上限设置在sys/types.h中有定义 #define FD_SETSIZE 256
readfds:select()函数监视的可读描述符集合 wtitefds:select()函数监视的可写描述符集合 errnofds:select()函数监视的异常描述符集合
timeout:select()函数监视超时结束时间,取NULL表示永久等待 返回值:返回总的位数这些位对应已准备好的描述符,否则返回-1 相关宏操作:
FD_ZERO(fd_set *fdset):清空fdset与所有描述符的关系 FD_SET(int fd, d_set * fdset):建立描述符fd与fdset得关系 FD_CLR(int fd, d_set * fdset):撤销描述符fd与fdset得关系
FD_ISSET(int fd, d_set * fdset):检查与fdset联系的描述符fd是否可以读写,返回非零表示可以读写
5.select()函数实现IO多路复用的步骤 (1)清空描述符集合
(2)建立需要监视的描述符与描述符集合的关系 (3)调用select函数
(4)检查监视的描述符判断是否已经准备好 (5)对已经准备好的描述符进程IO操作
表的存放规则:
fd_set readfds; FD_ZERO(&readfds); //将表清空
FD_SET(0,&readfds); //将0 加入 readfds
FD_SET(3,&readfds); //将3 加入 readfds
FD_SET(4,&readfds); //将4 加入 readsds
表头:
|
| 1 0 0 1 1 0 ...........………… 0
| | | | |
|
| 0 1 2 3 4 5
1023
n = select(4+1,&readfds,NULL,NULL,NULL); 检测就绪,返回就绪个数,未就绪的清零 (由于处理器的速度很快,n通常为1)
若此时0就绪:(n = 1) 表头:
|
| 1 0 0 0 0 0 ...........………… 0
| | | | |
|
| 0 1 2 3 4 5
1023
若此时有0 和3同时就绪(n = 2) 表头:
|
| 1 0 0 1 0 0 ...........………… 0
| | | | |
|
| 0 1 2 3 4 5
1023
判断是那个fd就序
If(FD_ISSET(fd,&readfds) == 1)
{
。。。。。。。
}
例:
int sockfd,maxfd,n; int connectfd ,fd; char buf[BUFF_SIZE];
fd_set readfds, tempfds;
maxfd = sockfd;
FD_ZERO(&readfds);
tempfds = readfds;
while(1) { tempfds = readfds; FD_SET(sockfd,&tempfds); //如:有50 client,某一时刻只有sockfd就绪
if(-1 == (n = select(maxfd + 1,&tempfds,NULL,NULL,NULL)))
exit(-1);
for(fd = 0; fd
{
if(FD_ISSET(fd,&tempfds)) //套接字两种都有可能就绪,如果不是sockfd,那么必定是以连接的套接字
{
if(fd == sockfd) { if(-1 == (connectfd = accept(sockfd,NULL,NULL)))
exit(-1); puts(\"hander shake !!!\\n\");
FD_SET(connectfd,&readfds); //将新的客户端添加至只读表
maxfd = maxfd > connectfd ? maxfd : connectfd; //时刻保证maxfd 最大
}else //不能用if(fd == connectfd) { bzero(buf,BUFF_SIZE);
if(-1 == (n = recv(fd,buf,BUFF_SIZE,0)))
exit(-1); if(n == 0) {
FD_CLR(fd,&readfds); //将退出的客户端从只读表清除 close(fd);
if(maxfd == fd) {
while(1) {
maxfd --;
if(!FD_ISSET(maxfd,&readfds)) //maxfd 是最后一个需
要监测的
continue;
else {
break;
}
}
}
}
printf(\"[%d] client buf:%s\\n\",n,buf);
}
} } }
获取套接字属性信息
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
sockfd套接字描述符 level选项级别SOL_SOCKET(man 7 solcket)(通用套接字)
IPPROTO_IP
(man 7 ip)得到选项名
IPPROTO_TCP
(man 7 tcp) optname选项名
SO_BROADCAST(广播)……
optval存放获取到的选项值的缓冲区地址&n
int n; optlen存放缓冲区长度的地
&len
int len = sizeof(n) 成功返回 0
失败返回 -1
第四天
设置套接字属性信息
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
sockfd套接字描述符
evel选项级别SOL_SOCKET
(man 7 solcket) (通用套接字)
IPPROTO_IP
(man 7 ip)得到选项名
IPPROTO_TCP
(man 7 tcp) optname选项名
SO_BROADCAST(广播)IP_ADD_MEMBERSHIP(组播)
组播结构体:struct ip_mreqn { truct in_addr imr_multiaddr; /* 组播ip
struct in_addr imr_addre; /* 服务器ip
int imr_ifindex;
/* interface index */通常为0
}; optval存放需要设置的选项值的缓冲区地址&n int n = 1(打开广播), int n = 0(关闭广播) optlen存放缓冲区长度的地址
sizeof(n) 成功返回 0
失败返回 -1
Int on = 1 setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) 使先前进程创建的端口能重新绑定
一单播广播组播
1.单播:接收方为一个,用户发送的包,可以到达指定的主机,一对一的通信, 数据包经过路由器或者交换机,不经过复制,需要转发
好处:服务器可以及时的为客户端响应 坏处:如果客户端的个数太大,会造成超载
host1 -------route/swith(转发)-----------> host2 2.广播:接收方为局域网内,所有主机, 属于一对所有,数据包经过路由器或者交换机 需要经过复制,转发,不存在CS,存在发送方,和接收方,使用UDP 注意:默认不允许发送 好处:效率高
坏处:如果大量发送会造成广播风暴
广播地址:主机号为全1,如,C类私有网络:192.168.1.255 广播MAC :FF:FF:FF:FF:FF:FF
host1------------route/swith(复制,转发) ------------------->host2
|
-------->host3
|
....
|
-------->host254
udp广播发送方:默认不允许
1).创建数据报套接字(填充地址结构体(广播IP))
2).设置套接字属性,允许发送广播包 (setsockopt) int on = 1;
setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(int ))(设置属性)
int on ; socklen_t len ; len = sizeof(on); getsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&on,&len)
2).发送数据报到广播ip
udp广播接收方:默认允许
1).创建数据报套接字(填充地址结构体(广播ip))
2).绑定广播地址(ip)
3).直接接收对方ip
3.组播
接收方为局域网多个主机,将具有相同需求的主机加入一个组,然后组内任何一个主机的包,组内所有成员都会收到,是广播的优化 优点:有针对性,相对广播可以降低网络带宽 缺点:相对单播,缺少校错机制
组播地址:D类IP 224.0.0.0 - 239.255.255.255 组播的MAC:01:00:5e:*.*.* (IP地址的低23bit)
(获取属性) host1------route(IGMP 网络组管理协议网络层)/swith(复制,转发) ------->host2
|
------------------------->host3
|
....
|
-------->host(多个
Udp组播发送方
1).创建数据报套接字(填充地址结构体组播ip)
2).直接发送组播ip
Udp组播接收方
1).创建数据报套接字(填充地址结构体组播IP)
2).绑定组播地址(ip)
3).设置属性,将当前主机ip加入组(IGMP) //需要路由器
struct ip_mreqn mulgroup; bzero(&mulgroup,sizeof(mulgroup)); mulgroup.imr_multiaddr.s_addr = inet_addr(组播ip);
mulgroup.imr_addre.s_addr = inet_addr(本机ip); mulgroup.imr_ifindex = 0;
setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mulgroup,sizeof(mulgroup))
4).直接接收对方ip
第五天: 说明:
网络中套接字通常是,当没有相应资源时是阻塞的,如果有资源可读,就会直接返回 网络中环境是异常复杂的,这个时候我们对异常处理,需要超时检测
一超时检测
1.设置套接字选项
Struct timeval tv; 套接字超时属性 (结构体)
tv.tv_sec = 3; 秒
tv.tv_usec = 1000; 微秒
setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)) connectfd = accept(sockfd,NULL,NULL) connectfd 继承sockfd的属性(这里应用的是超时属性) if(errno == EAGAIN)continue;
2.select
//如果没有任何一个fd就绪则超时,超时返回0,每次超时之后,tv值不会重置,需要用户自己重置
struct timeval tv; tv.tv_sec = 2; tv.tv_usec = 0; n = select(sockfd + 1,&readfds,NULL,NULL,&tv)) if(n == 0) {
printf(\"timeout %d ....\\n\",++count);
continue; }
3.alarm信号 //特性不会阻塞,会更新
/* function: 中断当前进程阻塞的系统调用,在哪里阻塞在哪里中断
signum: 捕捉的信号
act: 设置之后的信号属性
oldact: 获取先前默认的属性
*
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int); //signal 的信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); //sigaction最早的信号处理函数
sigset_t
sa_mask; //信号屏蔽码,可以屏蔽指定信号
int
sa_flags; //信号属性
void (*sa_restorer)(void); //linux 不支持
};
二 UNIX 域套接字编程
1.本地(本机)进程间通信
2.不经过OSI /TCP/IP 体系结构,不存在打包和拆包过程
3.可以完全套用TCP/UDP CS 模型
练习: 实现UNIX本地进程通信
三 tftp 实现