LINUXC 网络编程

网络模型

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 ;
端口可以根据端口本身和协议类型划分:
以端口划分
按照端口号分类:

  1. 公认端口:0~1023。它们紧密绑定于一些服务,通常这些端口的通讯明确表明了某种服务的协议,如:80端口对应与2. HTTP通信,21端口绑定与FTP服务,25端口绑定于SMTP服务,135端口绑定与RPC(远程过程调用)服务。
  2. 注册端口:1024~49151。它们松散的绑定于一些服务,也就是说有许多服务绑定于这些端口,这些端口同样用于其他许多目的,如:许多系统处理端口从1024开始
  3. 动态或私有端口:49152~65535。理论上,不应为服务分配这些端口,通常机器从1024开始分配动态端口。例外:SUN的RPC端口从32768开始。
    按照协议类型分类:
  4. TCP端口:即传输控制协议端口,需要在客户端和服务器之间建立连接,这样可以提供可靠的数据传输。常见的包括FTP的21端口,Telnet的23端口,SMTP的25端口,HTTP的80端口。
  5. UDP端口:即用户数据报协议端口,无需在客户端和服务器端建立连接,安全性得不到保障。常见的DNS的53端口,SNMP(简单网络管理协议)的161端口,QQ使用的8000和4000端口。
  6. 保留端口: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,则正确

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值