网络模型
OSI七层网络模型
TCP/IP 四层网络模型
通用四层协议
TCP协议
有连接的、可靠的、无记录边界的字节流协议。
无记录边界可能导致粘包问题。
TCP协议格式
各项含义
**序号:**该字段用来标识TCP源端设备向目的端设备发送的字节流,它表示在这个报文段中的第几个数据字节。序列号是一个32位的数。
确认号: TCP使用32位的确认号字段标识期望收到的下一个段的第一个字节,并声明此前的所有数据已经正确无误地收到,因此,确认号应该是上次已成功收到的数据字节序列号加1。收到确认号的源计算机会知道特定的段已经被收到。确认号的字段只在ACK标志被设置时才有效。
标志位:
URG:紧急标志位,说明紧急指针有效;
• ACK:确认标志位,多数情况下空,说明确认序号有效; 取1时表示应答字段有效,也即TCP应答号将包含在TCP段中,为0则反之。
• PSH:推标志位,置位时表示接收方应立即请求将报文交给应用层;
• RST:复位标志,用于重建一个已经混乱的连接,用来复位产生错误的连接,也会用来拒绝错误和非法的数据包。
• SYN:同步标志,该标志仅在三次握手建立TCP连接时有效
• FIN:结束标志,表示发送端已经发送到数据末尾,数据传送完成,发送FIN标志位的TCP段,连接将被断开。
TCP协议连接流程
三次握手
三次握手的意义: 确认客户端和服务端都是收发数据正常
第一次握手 TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT 同步已发送状态
第二次握手 TCP服务器收到请求报文后,如果同意连接,则会向客户端发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了 SYN-RCVD 同步收到状态 (connect 函数返回时间)
第三次握手 TCP客户端收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED已建立连接状态 触发三次握手(accept 函数返回时间)
四次挥手
数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。
第一次挥手 客户端发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入***FIN-WAIT-1(终止等待1)***状态
第二次挥手 服务器端接收到连接释放报文后,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT 关闭等待状态
第三次挥手 客户端接收到服务器端的确认请求后,客户端就会进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文,服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,服务器就进入了LAST-ACK**(最后确认)状态,等待客户端的确认。
第四次挥手 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态,但此时TCP连接还未终止,必须要经过2MSL后(最长报文寿命),当客户端撤销相应的TCB后,客户端才会进入CLOSED关闭状态,服务器端接收到确认报文后,会立即进入CLOSED关闭状态,到这里TCP连接就断开了,四次挥手完成
为什么客户端要等待2MSL?
主要原因是为了保证客户端发送那个的第一个ACK报文能到到服务器,因为这个ACK报文可能丢失,并且2MSL是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃,这样新的连接中不会出现旧连接的请求报文。
TCP流量控制
TCP编程
发送(客户端)
1、创建socket
2、填充结构体 (bzeor() 清零函数)
3、创建连接 connect
4、发送数据 send (udp 为 sendto)
5、接收数据 recv (udp recvfrom) recv返回0,表示断开连接
接收(服务端)
1、创建socket
2、绑定ip 与 端口 bind
3、设置套接字为监听状态,建立监听队列 listen
4、与客户端三次握手建立连接 accept,成功创建一个新的套接字
5、使用新的套接字 接收发送消息 send recv
TCP粘包
原因:
解决方法:
1、使用定长的数据包
2、发送时 数据长度+数据(发送不定长数据)
TCP多线程并发服务器
服务器端
include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "tcp_fun.h"
#include <unistd.h>
#include <pthread.h>
int main(int argc,char **argv)
{
if(3 != argc)
{
printf("using:%s IP PORT\n",argv[0]);
exit(EXIT_FAILURE);
}
int sockfd,err,len;
ssa_in local_add;
ssa_in remote_add;
len = SSA_IN_LEN;
bzero(&local_add,SSA_IN_LEN);
sockfd = socket(AF_INET,SOCK_STREAM, 0);
if(-1 == sockfd)
{
perror("socket");
exit(EXIT_FAILURE);
}
local_add.sin_family = AF_INET;
local_add.sin_port = htons(atoi(argv[2]));
local_add.sin_addr.s_addr = inet_addr(argv[1]);
err = bind(sockfd,(ssa *)&local_add,len);
if(-1 == err)
{
perror("bind");
exit(EXIT_FAILURE);
}
err = listen(sockfd,CONNECT_NUM);
if(-1 == err)
{
perror("listen");
exit(EXIT_FAILURE);
}
while(1)
{
bzero(&remote_add,SSA_IN_LEN);
int new_sockfd = accept(sockfd,(ssa *)&remote_add,(socklen_t *)&len);
if(-1 == new_sockfd)
{
perror("accept");
continue;
}
printf("connect %s:%d \n",inet_ntoa(remote_add.sin_addr),ntohs(remote_add.sin_port));
pthread_t thread;
err = pthread_create(&thread,NULL, new_client,(void *)&new_sockfd);
if(-1 == err)
{
printf("pthread create:%s\n",strerror(err));
}
}
}
客户端
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "tcp_fun.h"
#include <unistd.h>
#include <pthread.h>
int main(int argc,char **argv)
{
if(3 != argc)
{
printf("using:%s IP PORT\n",argv[0]);
exit(EXIT_FAILURE);
}
int sockfd,err,len;
ssa_in local_add;
ssa_in remote_add;
len = SSA_IN_LEN;
char buff[128]={0};
bzero(&local_add,SSA_IN_LEN);
sockfd = socket(AF_INET,SOCK_STREAM, 0);
if(-1 == sockfd)
{
perror("socket");
exit(EXIT_FAILURE);
}
local_add.sin_family = AF_INET;
local_add.sin_port = htons(atoi(argv[2]));
local_add.sin_addr.s_addr = inet_addr(argv[1]);
err = connect(sockfd,(ssa *)&local_add,len);
if(-1 == err)
{
perror("connect");
exit(EXIT_FAILURE);
}
while(1)
{
bzero(&remote_add,SSA_IN_LEN);
bzero(buff,sizeof(buff));
printf("plaese input:\n");
fgets(buff,sizeof(buff)-1,stdin);
if(strstr(buff,"\n"))
{
buff[strcspn(buff,"\n")] = '\0';
}
err = send(sockfd,buff,sizeof(buff)-1,0);
if(-1 == err)
{
perror("send");
continue;
}
if(0 == strncmp(buff,"exit",4))
{
close(sockfd);
break;
}
bzero(buff,sizeof(buff));
err = recv(sockfd,buff,sizeof(buff), 0);
if(-1 == err)
{
perror("recv");
continue;
}
if(0 == err)
{
printf("service exit \n");
close(sockfd);
break;
}
printf("%s\n",buff);
}
return 0;
}
线程函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "tcp_fun.h"
#include <unistd.h>
#include <pthread.h>
void * new_client(void * arg)
{
int sockfd = *(int *)arg;
int rcv;
char buff[128]={0};
//int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
ssa_in add;
int len = SSA_IN_LEN;
bzero(&add, sizeof(add));
rcv = getpeername(sockfd,(ssa *)&add,(socklen_t *)&len);
if(-1 != rcv)
{
printf("ip:%s port%d\n",inet_ntoa(add.sin_addr),ntohs(add.sin_port));
}
rcv = pthread_detach(pthread_self());
if(0 != rcv)
{
printf("pthread_detach:%s",strerror(rcv));
}
while(1)
{
bzero(buff,sizeof(buff));
rcv = recv(sockfd,buff,sizeof(buff)-1,0);
if(-1 == rcv)
{
printf("recv:%s",strerror(rcv));
continue;
}
if(0 == rcv)
{
printf("client exit\n");
close(sockfd);
break;
}
printf("%s:%d: %s \n",inet_ntoa(add.sin_addr),ntohs(add.sin_port),buff);
send(sockfd,"recv ok",sizeof("recv ok"),0);
if(0 == strncmp(buff,"exit",sizeof("exit")))
{
printf("thread %ld exit\n",pthread_self());
close(sockfd);
break;
}
}
pthread_exit(NULL);
}
头文件
#ifndef __TCP_FUN_H
#define __TCP_FUN_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define SSA_IN_LEN sizeof(struct sockaddr_in)
#define CONNECT_NUM 10
typedef struct sockaddr_in ssa_in;
typedef struct sockaddr ssa;
void * new_client(void * arg);
#endif /*__TCP_FUN_H*/
makefile文件
CC=gcc
service client:tcp_service.o tcp_client.o tcp_fun.o
gcc tcp_service.o tcp_fun.o -o service -lpthread
gcc tcp_client.o tcp_fun.o -o client -lpthread
mv service client tcp_service.o tcp_client.o tcp_fun.o ./linux/
tcp_service.o:tcp_service.c tcp_fun.h
gcc -c tcp_service.c -o tcp_service.o
tcp_client.o:tcp_client.c tcp_fun.h
gcc -c tcp_client.c -o tcp_client.o
tcp_fun.o:tcp_fun.c tcp_fun.h
gcc -c tcp_fun.c -o tcp_fun.o
.PHONY:clean
clean:
rm -rf ./linux/*
UDP协议
UDP协议格式
UDP长度:长度为16位,该字段值为报头和数据两部分的总字节数。
UDP检验和(Checksum):长度为16位,UDP检验和作用于UDP报头和UDP数据的所有位。由发送端计算和存储,由接收端校验。
UDP数据校验
UDP编程
发送
1、创建socket函数
int socket(int domain, int type, int protocol);
udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
2、sendto,发送函数
发送端代码
接收
1、创建socket
2、绑定ip和端口到socket bind
3、接收数据 recvfrom
UDP多进程并发服务器
服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*udp 多进程服务器端*/
/*
1、创建主进程,用与接收客户端请求
2、接收客户端请求后,分配新的socket以及端口用于和客户端连接
*/
void send_client(struct sockaddr_in *seradd,struct sockaddr_in *newcadd,struct sockaddr_in *newclient);
int main(int argc,char *argv[]){
/*变量定义*/
int sfd; //主进程socket
int len;//记录结构体长度
int rev;//接收函数返回值
struct sockaddr_in seradd;//填充服务端信息
struct sockaddr_in cliadd;//接收客户端信息
char buf[1024] = {0}; //接收数据缓冲区
pid_t cpid; //接收子进程号
/*代码*/
len = sizeof(seradd);
//创建主进程
//1、创建socket
sfd = socket(AF_INET,SOCK_DGRAM,0);
if(sfd == -1){
perror("error in socket():");
exit(EXIT_FAILURE);
}
//2、填充信息到结构体
memset(&seradd,0,len);
seradd.sin_family = AF_INET;
seradd.sin_port = htons(atoi(argv[2]));
seradd.sin_addr.s_addr = inet_addr(argv[1]);
//3、绑定ip和端口至socket
rev = bind(sfd,(struct sockaddr *)&seradd,len);
if(rev == -1){
perror("error in bind():");
exit(EXIT_FAILURE);
}
//接收数据
while(1){
memset(buf,0,sizeof(buf));
memset(&cliadd,0,len);
rev = recvfrom(sfd,buf,sizeof(buf),0,(struct sockaddr *)&cliadd,&len);
if(rev == -1){
perror("error in recvfrom():");
exit(EXIT_FAILURE);
}
//输出客户端信息
printf("The connect from:\033[32m %s %d\033[0m\n",inet_ntoa(cliadd.sin_addr),ntohs(cliadd.sin_port));
printf("data : \033[31m %s \033[0m\n",buf);
//创建子进程
cpid = fork();
if(cpid == -1){
perror("error in fork():");
exit(EXIT_FAILURE);
}
else if(cpid == 0){
struct sockaddr_in new_seradd;//创建新的端口等
struct sockaddr_in new_client;//创建新的端口等
memset(&new_seradd,0,len);
memset(&new_client,0,len);
new_client = cliadd;
send_client(&seradd,&new_seradd,&new_client);
exit(EXIT_SUCCESS);
}
}
return 0;
}
/*函数*/
void send_client(struct sockaddr_in *seradd,struct sockaddr_in *newcadd,struct sockaddr_in *newclient){
struct sockaddr_in * new_ser; //接收目的ip,并分配新port
struct sockaddr_in * client; //接收目的ip,并分配新port
int sfd;
int len;
int rev;
char buf[1024]={0};
len = sizeof(struct sockaddr_in);
new_ser = newcadd;
client = newclient;
sfd = socket(AF_INET,SOCK_DGRAM,0);
if(sfd == -1){
perror("error in socket():");
exit(EXIT_FAILURE);
}
//输出客户端信息
new_ser->sin_family = seradd->sin_family;
new_ser->sin_port = htons(0);
new_ser->sin_addr.s_addr = seradd->sin_addr.s_addr;
rev = bind(sfd,(struct sockaddr *)new_ser,len);
if(rev == -1){
perror("error in rev():");
exit(EXIT_FAILURE);
}
printf("以新端口发送数据\n");
rev = sendto(sfd,"SEND NEW PORT",sizeof("SEND NEW PORT"),0,(struct sockaddr*)client,len);
if(rev == -1){
perror("error in sendto():");
exit(EXIT_FAILURE);
}
printf("以新端口接收数据\n");
while(1){
memset(buf,0,sizeof(buf));
rev = recvfrom(sfd,buf,sizeof(buf),0,(struct sockaddr *)new_ser,&len);
if(rev == -1){
perror("error in recvfrom():");
exit(EXIT_FAILURE);
}
printf("data:\033[32m%s\n\033[0m",buf);
}
close(sfd);
return ;
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*udp 多进程客户端*/
/*
1、创建socket,并发送一条信息
2、接收打一条信息 (struct sockaddr_in.sin_port 新更换端口)
*/
int main(int argc,char *argv[]){
/*变量定义*/
int sfd;
int len;
int rev;
struct sockaddr_in seradd;
char buf[1024] = {0};
/*代码*/
len = sizeof(seradd);
//创建主进程
//1、创建socket
sfd = socket(AF_INET,SOCK_DGRAM,0);
if(sfd == -1){
perror("error in socket():");
exit(EXIT_FAILURE);
}
//2、填充信息到结构体
memset(&seradd,0,len);
seradd.sin_family = AF_INET;
seradd.sin_port = htons(atoi(argv[2]));
seradd.sin_addr.s_addr = inet_addr(argv[1]);
//3、发送信息获取端口号
rev = sendto(sfd,"CONNECT",strlen("CONNECT"),0,(struct sockaddr *)&seradd,len);
if(rev == -1){
perror("error in sendto():");
exit(EXIT_FAILURE);
}
//4、接收新的端口号
memset(buf,0,sizeof(buf));
memset(&seradd,0,len);
//接收新的端口号至结构体
rev = recvfrom(sfd,buf,1024,0,(struct sockaddr *)&seradd,&len);
if(rev == -1){
perror("error in recvfrom when acquire new port:");
exit(EXIT_FAILURE);
}
printf("接收新端口成功!!!\n");
printf("请发送数据:\n");
//正常接收发送数据
while(1){
memset(buf,0,sizeof(buf));
fgets(buf,1024,0);
// scanf("%s",buf);
// printf("\n");
printf("接收数据:%s\n",buf);
rev = sendto(sfd,buf,sizeof(buf),0,(struct sockaddr *)&seradd,len);
if(rev == -1){
perror("error in sendto():");
exit(EXIT_FAILURE);
}
rev = strncmp("exit",buf,4);
printf("rev = %d\n",rev);
if(rev == 0){
printf("退出!!!!\n");
break;
}
/* rev = recvfrom(sfd,buf,1024,0,(struct sockaddr *)&seradd),&len);
if(rev == -1){
perror("error in recvfrom when acquire new port:");
exit(EXIT_FAILURE);
}
*/
}
/*资源关闭*/
close(sfd);
return 0;
}
端口重用技术
端口是什么
端口是用于在主机之间标识不同进程的一种逻辑概念,ip地址标识了一台主机,ip + port + 协议类型 = 一个具体的进程;
端口由一个16位的数据标识,范围为 0 ~ 65535 ;
端口可以根据端口本身和协议类型划分:
以端口划分
按照端口号分类:
- 公认端口:0~1023。它们紧密绑定于一些服务,通常这些端口的通讯明确表明了某种服务的协议,如:80端口对应与2. HTTP通信,21端口绑定与FTP服务,25端口绑定于SMTP服务,135端口绑定与RPC(远程过程调用)服务。
- 注册端口:1024~49151。它们松散的绑定于一些服务,也就是说有许多服务绑定于这些端口,这些端口同样用于其他许多目的,如:许多系统处理端口从1024开始
- 动态或私有端口:49152~65535。理论上,不应为服务分配这些端口,通常机器从1024开始分配动态端口。例外:SUN的RPC端口从32768开始。
按照协议类型分类: - TCP端口:即传输控制协议端口,需要在客户端和服务器之间建立连接,这样可以提供可靠的数据传输。常见的包括FTP的21端口,Telnet的23端口,SMTP的25端口,HTTP的80端口。
- UDP端口:即用户数据报协议端口,无需在客户端和服务器端建立连接,安全性得不到保障。常见的DNS的53端口,SNMP(简单网络管理协议)的161端口,QQ使用的8000和4000端口。
- 保留端口:UNIX有保留端口号的概念,只有超级用户特权的进程才允许给它自己分配一个保留端口号。这些端口号介于1~1023之间,一些应用程序将它作为客户与服务器认证的一部分。
端口重用是什么
当网络编程时,如何进程A 使用 TCP的socket 并 使用10086端口,那么进程B使用 TCP的socket就不能使用10086端口,端口复用技术允许多个socket绑定到同一个端口,即进程B使用 TCP的socket就也能和进程A一起使用10086端口。
端口重用的实现原理(待补充)
端口重用编程
int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
多路复用
什么是多路复用
多路: 指的是多个socket网络连接;
复用: 指的是复用一个线程、使用一个线程来检查多个文件描述符(Socket)的就绪状态
多路复用主要有三种技术:select,poll,epoll。epoll是最新的, 也是目前最好的多路复用技术;
IO多路复用——深入浅出理解select、poll、epoll的实现
I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
目前支持I/O多路复用的系统调用有select,pselect,poll,epoll。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
与多线程和多进程相比IO多路复用的最大好处就是开销小。
多路复用的应用场景
select
int select (int nfd, fd_set *readfds, fd_set *writefds, fd_set*exceptfds, struct timeval *timeout);
**readfds:**内核检测该集合中的IO是否可读。如果想让内核帮忙检测某个IO是否可读,需要手动把文件描述符加入该集合。
**writefds:**内核检测该集合中的IO是否可写。同readfds,需要手动把文件描述符加入该集合。
**exceptfds:**内核检测该集合中的IO是否异常。同readfds,需要手动把文件描述符加入该集合。
**nfds:**以上三个集合中最大的文件描述符数值 + 1,例如集合是{0,1,5,10},那么 maxfd 就是 11
**timeout:**用户线程调用select的超时时长。
设置成NULL,表示如果没有 I/O 事件发生,则 select 一直等待下去。
设置为非0的值,这个表示等待固定的一段时间后从 select 阻塞调用中返回。
设置成 0,表示根本不等待,检测完毕立即返回。
函数返回值:
大于0:成功,返回集合中已就绪的IO总个数
等于-1:调用失败
等于0:没有就绪的IO
select返回时只有时间发生的标志位才会被置一
- 执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
- 若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
- 若再加入fd=2,fd=1,则set变为0001,0011
- 执行select(6,&set,0,0,0)阻塞等待
- 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空
UDP组播的实现
组播的概念
多播技术,也被称为“组播”,是一种网络通信机制,它允许一个节点(发送者)向一组特定的节点(接收者)发送信息。这种方式在网络编程中非常有用,因为它可以大大提高效率和性能,同时减少网络带宽的使用。
在单播通信中,信息从一个节点发送到另一个节点,而在广播通信中,信息从一个节点发送到网络中的所有节点。多播则介于这两者之间,信息从一个节点发送到一组特定的节点。这种方式特别适合于需要向一组特定的主机发送信息的场景,例如在线视频会议或实时数据共享。
多播是一种在网络中发送信息的方式,它允许一个节点向一组节点发送信息,而不是单独向每个节点发送。这种方式在网络编程中非常有用,因为它可以大大提高效率和性能。
在多播编程中,我们使用一些特殊的套接字选项来控制多播行为。这些选项通常在IP层设置,因为多播是在IP层实现的。
组播地址
多播的实现依赖于D类IP地址,范围从224.0.0.0到239.255.255.255。这些地址被划分为局部链接多播地址、预留多播地址和管理权限多播地址。主机可以向路由器请求加入或退出某个多播组,然后路由器和交换机会有选择地复制并传输数据,只将数据传输给组内的主机。
在IP网络中,多播地址被定义为D类IP地址,范围从224.0.0.0到239.255.255.255。这些地址被划分为三类:
局部链接多播地址:这个范围是从224.0.0.0到224.0.0.255。预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用。这些地址主要用于网络设备之间的本地通信,例如路由器之间的通信。这些地址的数据包不会被路由器转发到其他网络。
预留多播地址:这个范围是从224.0.1.0到238.255.255.255。这些地址可以在全球范围内使用,例如在Internet上。这意味着,如果你的应用程序需要向全球的多个节点发送信息,你可以使用这个范围内的地址。224.0.1.0~224.0.1.255是公用组播地址,可以用于Internet。224.0.2.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效;
管理权限多播地址:这个范围是从239.0.0.0到239.255.255.255。这些地址类似于私有IP地址,只能在特定的组织或企业内部使用。这些地址的数据包不能在Internet上路由,因此可以用来限制多播的范围。
组播编程步骤
-
在编写多播程序时,我们通常遵循以下步骤:
-
创建一个套接字:我们首先需要创建一个套接字来发送和接收数据。
-
设置多播参数:然后,我们需要设置多播的参数,如TTL和本地回环。
-
加入多播组:接下来,我们需要将节点加入到一个多播组中。(IP层面)
-
发送和接收数据:一旦节点加入了一个多播组,它就可以开始发送和接收数据了。
-
离开多播组:最后,当节点不再需要接收多播数据时,它可以从多播组中离开。
-
组播在IP层实现
组播的编程
发送端
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "udp_group.h"
int main(int argc,char ** argv)
{
int sockfd,err,ssa_in_len;
ssa_in_len = SSA_IN_LEN;
char sendbuff[128]={0};
//create socket
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(-1 == sockfd)
{
perror("socket");
exit(-1);
}
//set boardcast
uint8_t ttl = 1;
if (setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)) < 0)
{
perror("setsockopt");
exit(-1);
}
//set add and port
ssa_in send_add;
ssa_in recv_add;
bzero(&send_add,SSA_IN_LEN);
bzero(&recv_add,SSA_IN_LEN);
send_add.sin_family = AF_INET;
send_add.sin_addr.s_addr = inet_addr(UDP_GROUP_IP);
send_add.sin_port = htons(UDP_GROUP_PORT);
while(1)
{
//recv send data
bzero(sendbuff,sizeof(sendbuff));
printf("wait input send data:\n");
while(NULL != fgets(sendbuff,(sizeof(sendbuff)),stdin))
{
if(NULL != strstr(sendbuff, "\n"))
{
*(strstr(sendbuff, "\n")) = '\0';
sendto(sockfd, sendbuff,sizeof(sendbuff),0,(ssa *)&send_add,SSA_IN_LEN );
break;
}
sendto(sockfd, sendbuff,sizeof(sendbuff),0,(ssa *)&send_add,SSA_IN_LEN );
bzero(sendbuff,sizeof(sendbuff));
}
}
return 0;
}
接收端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "udp_group.h"
int main(int argc,char ** argv)
{
int sockfd,err,ssa_in_len;
ssa_in_len = SSA_IN_LEN;
char recvbuff[128]={0};
//create socket
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(-1 == sockfd)
{
perror("socket");
exit(-1);
}
//set add and port
ssa_in send_add;
ssa_in recv_add;
bzero(&send_add,SSA_IN_LEN);
bzero(&recv_add,SSA_IN_LEN);
send_add.sin_family = AF_INET;
send_add.sin_addr.s_addr = htonl(INADDR_ANY);
send_add.sin_port = htons(UDP_GROUP_PORT);
err = bind(sockfd,(ssa *)&send_add,SSA_IN_LEN);
if(-1 == err)
{
perror("bind");
exit(-1);
}
//join group
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr(UDP_GROUP_IP); // 多播地址
mreq.imr_interface.s_addr = htonl(INADDR_ANY); // 任意网络接口
if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
perror("setsockopt");
return -1;
}
while(1)
{
//recv send data
bzero(recvbuff,sizeof(recvbuff));
printf("wait recv data:\n");
while(recvfrom(sockfd,recvbuff,sizeof(recvbuff)-1,0, (ssa *)&recv_add,(socklen_t *)&ssa_in_len) > 0)
{
printf("%s:%d send data:%s\n",inet_ntoa(recv_add.sin_addr),ntohs(recv_add.sin_port),recvbuff);
bzero(recvbuff,sizeof(recvbuff));
bzero(&recv_add,SSA_IN_LEN);
}
}
return 0;
}
头文件
ifndef __UDP_GROUP_H
#define __UDP_GROUP_H
#include <netinet/in.h>
#include <sys/socket.h>
#define UDP_GROUP_IP "224.0.2.4"
#define UDP_GROUP_PORT 9999
#define SSA_IN_LEN sizeof(struct sockaddr_in)
typedef struct sockaddr_in ssa_in;
typedef struct sockaddr ssa;
#endif
各种网络协议
IP协议
ARP协议
ICMP协议
C语言网络编程
socket
Linux提供的socket
socket套接字类型
1、流式套接字(TCP)
2、数据报套接字(UDP)
3、原始套接字
网络数据流在cpu中以不同的方式存储,有小端和大端两种方式(小端与人的读写同向,大端与人的读逆向)
网络传输时先判断是否为小端,若为小端则进行转换。可以用共用体判断是大端还是小端存储。
主机与网络字节序转换
inet_addr、inet_aton(ip转换)
inet_ntoa 网络字节序转换为IP字符串
端口转换为网络字节序
网络字节序转换为端口
atoi (字符串转换为整数)
getsockname getpeername
getsockname函数用于获取与某个套接字关联的本地协议地址
getpeername函数用于获取与某个套接字关联的外地协议地址
对于这两个函数,如果函数调用成功,则返回0,如果调用出错,则返回-1。
使用这两个函数,我们可以通过套接字描述符来获取自己的IP地址和连接对端的IP地址,如在未调用bind函数的TCP客户端程序上,可以通过调用getsockname()函数获取由内核赋予该连接的本地IP地址和本地端口号,还可以在TCP的服务器端accept成功后,通过getpeername()函数来获取当前连接的客户端的IP地址和端口号。
杂项
LINUX如何管理网络接口(网卡)
LINUX 创建socket时内核干了什么
LINUX UDP和TCP的socket可以绑定同一个端口吗 ?
可以,逻辑独立
LINUX UDP或TCP的socket(同一协议)可以绑定同一个端口吗 ?
端口复用 setsockopt
大端和小端区别
数据存储:小端模式和大端模式——终于搞明白了!!!
小端模式,符合人类的读写习惯;
大小端的不同在于字节存储的顺序,计算机最小存储单位为字节,字节内的顺序不能变,字节之间的顺序变化;
在不同的机器上解读数据,要注意是一次取8、16、32 那种位数,否则数据解读错误;
网络数据以大端模式传输;
网络协议数据校验
TCP校验和覆盖TCP首部和TCP数据,而IP首部中的校验和只覆盖IP的首部,不覆盖IP数据报中的任何数据。
TCP的校验和是必需的,而UDP的校验和是可选的。
TCP和UDP计算校验和时,都要加上一个12字节的伪首部。
伪首部共有12字节,包含IP首部的一些字段,有如下信息:32位源IP地址、32位目的IP地址、8位保留字节(置0)、8位传输层协议号(TCP是6,UDP是17)、16位TCP报文长度(TCP首部+数据)。
伪首部是为了增加TCP校验和的检错能力:通过伪首部的目的IP地址来检查TCP报文是否收错了、通过伪首部的传输层协议号来检查传输层协议是否选对了。
TCP、UDP数据校验
检验和计算过程
TCP首部校验和计算三部分:TCP首部+TCP数据+TCP伪首部。
发送端:
首先,把伪首部、TCP报头、TCP数据分为16位的字,如果总长度为奇数个字节,则在最后增添一个位都为0的字节。
把TCP报头中的校验和字段置为0。
其次,用反码相加法(对每16bit进行二进制反码求和)累加所有的16位字(进位也要累加,进位则将高位叠加到低位)。
最后,将上述结果作为TCP的校验和,存在检验和字段中。
接收端:
将所有反码相加,高位叠加到低位, 如计算结果的16位中每一位都为1,则正确,否则说明发生错误。
验证示例:
校验和 反码求和过程
以4bit 为例
发送端计算:
数据: 1000 0100 校验和 0000
则反码:0111 1011 1111
叠加: 0111+1011+1111 = 0010 0001 高于4bit的, 叠加到低4位 0001 + 0010 = 0011 即为校验和
接收端计算:
数据: 1000 0100 检验和 0011
反码: 0111 1011 1100
叠加: 0111 + 1011 +1100 = 0001 1110 叠加为4bit为1111. 全为1,则正确