传输层协议UDP

传输层

负责数据能够从发送端传输接收器。

再谈端口号

端口号标识了一个主机上进行通信的不同应用程序;

  • 在TCP/IP协议中,用"源IP","源端口号","目的IP","目的端口号"这样的一个五元组来标识一个通信

端口号的划分

端口一共分为 3 类

1. 知名端口(Well-known Ports)

范围:0 ~ 1023

  • 固定分配给常用系统服务
  • 普通程序不能随便用
  • 需要管理员权限

常见 UDP 知名端口:

  • 53:DNS(域名解析)
  • 67/68:DHCP(自动获取 IP)
  • 69:TFTP(简单文件传输)
  • 161:SNMP(网络管理)

2. 注册端口(Registered Ports)

范围:1024 ~ 49151

  • 给用户程序、第三方软件使用
  • 不需要管理员权限
  • 可以向 IANA 注册,但不强制

例子:

  • 1900:SSDP(设备发现)
  • 3478:STUN(NAT 穿透)

3. 临时端口 / 动态端口(Dynamic/Private Ports)

范围:49152 ~ 65535

  • 客户端临时自动使用
  • 用完就释放
  • 不需要手动分配

当你用 UDP 发消息时,操作系统会自动给你分配一个临时端口

常见的知名端口号

纯 UDP 服务

  • 53:DNS(域名解析)
  • 67 / 68:DHCP(自动获取 IP)
  • 69:TFTP(简单文件传输)
  • 123:NTP(网络时间同步)
  • 161:SNMP(网络设备管理)
  • 162:SNMP Trap(告警)

TCP服务,日常网络、服务器必用:

  • 20 / 21:FTP(文件传输)
  • 22:SSH(安全远程登录)
  • 23:Telnet(不安全远程登录)
  • 25:SMTP(发邮件)
  • 53:DNS(也用 TCP)
  • 80:HTTP(网页)
  • 110:POP3(收邮件)
  • 143:IMAP(收邮件)
  • 443:HTTPS(加密网页)
  • 3389:Windows 远程桌面

问题:

1.一个进程是否可以绑定多个端口号?

  • 一个进程(程序)同时绑定多个不同的端口号是非常正常、非常常见的操作,没有任何限制。
  • 同时提供多种服务
  • TCP和UDP分开
  • 区分不同的功能
  • 同一个协议 + 同一个端口 = 只能一个进程绑定
  • 不同协议 = 端口可以重复,进程 A:TCP 80 ,进程 B:UDP 80

UDP协议

  • 内核中UDP协议规定端口号就是16位的
  • 16位UDP长度负责UDP报头和报文分离。
  • UDP不存在数据粘包问题。
  • UDP是用户数据报,发送缓冲区收到一个数据报,就发一个数据报

如何理解UDP协议报头?

协议报头就是一个结构体,通信双方互传结构体变量。

如何理解报文?重新理解封装过程和解包过程?

操作系统内部可能会同时存在很多报文,那么操作系统就要对这些报文进行管理,先描述再组织。

如何理解一个报文:在内核中,struct sk_buff,任何一个报文在操作系统内部都会开辟一个缓冲区。

管理报文的思路和操作系统管理进程的思路一模一样。

cpp 复制代码
struct sk_buff {
	/* These two members must be first. */
	struct sk_buff		*next;
	struct sk_buff		*prev;

	struct sock		*sk;
	struct skb_timeval	tstamp;
	struct net_device	*dev;
	struct net_device	*input_dev;

	union {
		struct tcphdr	*th;
		struct udphdr	*uh;
		struct icmphdr	*icmph;
		struct igmphdr	*igmph;
		struct iphdr	*ipiph;
		struct ipv6hdr	*ipv6h;
		unsigned char	*raw;
	} h;

	union {
		struct iphdr	*iph;
		struct ipv6hdr	*ipv6h;
		struct arphdr	*arph;
		unsigned char	*raw;
	} nh;

	union {
	  	unsigned char 	*raw;
	} mac;

	struct  dst_entry	*dst;
	struct	sec_path	*sp;

	/*
	 * This is the control buffer. It is free to use for every
	 * layer. Please put your private variables there. If you
	 * want to keep them across layers you have to do a skb_clone()
	 * first. This is owned by whoever has the skb queued ATM.
	 */
	char			cb[48];

	unsigned int		len,
				data_len,
				mac_len,
				csum;
	__u32			priority;
	__u8			local_df:1,
				cloned:1,
				ip_summed:2,
				nohdr:1,
				nfctinfo:3;
	__u8			pkt_type:3,
				fclone:2,
				ipvs_property:1;
	__be16			protocol;

	void			(*destructor)(struct sk_buff *skb);
#ifdef CONFIG_NETFILTER
	struct nf_conntrack	*nfct;
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
	struct sk_buff		*nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
	struct nf_bridge_info	*nf_bridge;
#endif
	__u32			nfmark;
#endif /* CONFIG_NETFILTER */
#ifdef CONFIG_NET_SCHED
	__u16			tc_index;	/* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
	__u16			tc_verd;	/* traffic control verdict */
#endif
#endif
#ifdef CONFIG_NET_DMA
	dma_cookie_t		dma_cookie;
#endif
#ifdef CONFIG_NETWORK_SECMARK
	__u32			secmark;
#endif


	/* These elements must be at the end, see alloc_skb() for details.  */
	unsigned int		truesize;
	atomic_t		users;
	unsigned char		*head,
				*data,
				*tail,
				*end;
};
当我们收到一个报文的时候,我们是如何做到用文件原理,读数据到应用层?
cpp 复制代码
struct sock {
	/*
	 * Now struct inet_timewait_sock also uses sock_common, so please just
	 * don't add nothing before this first member (__sk_common) --acme
	 */
	struct sock_common	__sk_common;
#define sk_family		__sk_common.skc_family
#define sk_state		__sk_common.skc_state
#define sk_reuse		__sk_common.skc_reuse
#define sk_bound_dev_if		__sk_common.skc_bound_dev_if
#define sk_node			__sk_common.skc_node
#define sk_bind_node		__sk_common.skc_bind_node
#define sk_refcnt		__sk_common.skc_refcnt
#define sk_hash			__sk_common.skc_hash
#define sk_prot			__sk_common.skc_prot
	unsigned char		sk_shutdown : 2,
				sk_no_check : 2,
				sk_userlocks : 4;
	unsigned char		sk_protocol;
	unsigned short		sk_type;
	int			sk_rcvbuf;
	socket_lock_t		sk_lock;
	wait_queue_head_t	*sk_sleep;
	struct dst_entry	*sk_dst_cache;
	struct xfrm_policy	*sk_policy[2];
	rwlock_t		sk_dst_lock;
	atomic_t		sk_rmem_alloc;
	atomic_t		sk_wmem_alloc;
	atomic_t		sk_omem_alloc;
	struct sk_buff_head	sk_receive_queue;
	struct sk_buff_head	sk_write_queue;
	struct sk_buff_head	sk_async_wait_queue;
	int			sk_wmem_queued;
	int			sk_forward_alloc;
	gfp_t			sk_allocation;
	int			sk_sndbuf;
	int			sk_route_caps;
	int			sk_gso_type;
	int			sk_rcvlowat;
	unsigned long 		sk_flags;
	unsigned long	        sk_lingertime;
	/*
	 * The backlog queue is special, it is always used with
	 * the per-socket spinlock held and requires low latency
	 * access. Therefore we special case it's implementation.
	 */
	struct {
		struct sk_buff *head;
		struct sk_buff *tail;
	} sk_backlog;
	struct sk_buff_head	sk_error_queue;
	struct proto		*sk_prot_creator;
	rwlock_t		sk_callback_lock;
	int			sk_err,
				sk_err_soft;
	unsigned short		sk_ack_backlog;
	unsigned short		sk_max_ack_backlog;
	__u32			sk_priority;
	struct ucred		sk_peercred;
	long			sk_rcvtimeo;
	long			sk_sndtimeo;
	struct sk_filter      	*sk_filter;
	void			*sk_protinfo;
	struct timer_list	sk_timer;
	struct timeval		sk_stamp;
	struct socket		*sk_socket;
	void			*sk_user_data;
	struct page		*sk_sndmsg_page;
	struct sk_buff		*sk_send_head;
	__u32			sk_sndmsg_off;
	int			sk_write_pending;
	void			*sk_security;
	void			(*sk_state_change)(struct sock *sk);
	void			(*sk_data_ready)(struct sock *sk, int bytes);
	void			(*sk_write_space)(struct sock *sk);
	void			(*sk_error_report)(struct sock *sk);
  	int			(*sk_backlog_rcv)(struct sock *sk,
						  struct sk_buff *skb);  
	void                    (*sk_destruct)(struct sock *sk);
};
cpp 复制代码
struct inet_sock {
	/* sk and pinet6 has to be the first two members of inet_sock */
	struct sock		sk;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
	struct ipv6_pinfo	*pinet6;
#endif
	/* Socket demultiplex comparisons on incoming packets. */
	__u32			daddr;
	__u32			rcv_saddr;
	__u16			dport;
	__u16			num;
	__u32			saddr;
	__s16			uc_ttl;
	__u16			cmsg_flags;
	struct ip_options	*opt;
	__u16			sport;
	__u16			id;
	__u8			tos;
	__u8			mc_ttl;
	__u8			pmtudisc;
	__u8			recverr:1,
				is_icsk:1,
				freebind:1,
				hdrincl:1,
				mc_loop:1;
	int			mc_index;
	__u32			mc_addr;
	struct ip_mc_socklist	*mc_list;
	struct {
		unsigned int		flags;
		unsigned int		fragsize;
		struct ip_options	*opt;
		struct rtable		*rt;
		int			length; /* Total length of all frames */
		u32			addr;
		struct flowi		fl;
	} cork;
};
cpp 复制代码
struct udp_sock {
	/* inet_sock has to be the first member */
	struct inet_sock inet;
	int		 pending;	/* Any pending frames ? */
	unsigned int	 corkflag;	/* Cork is required */
  	__u16		 encap_type;	/* Is this an Encapsulation socket? */
	/*
	 * Following member retains the information to create a UDP header
	 * when the socket is uncorked.
	 */
	__u16		 len;		/* total length of pending frames */
};
  • udp_sock包含inet_sock,inet_sock包含sock
  • sock就是基类,网络协议栈向下传递的过程就是从sock到inet_sock到udp_sock的过程。

UDP的特点

面向数据报

应⽤层交给UDP多⻓的报⽂, UDP原样发送, 既不会拆分, 也不会合并;
⽤UDP传输100个字节的数据:
如果发送端调⽤⼀次sendto, 发送100个字节, 那么接收端也必须调⽤对应的⼀次recvfrom, 接收
100个字节; ⽽不能循环调⽤10次recvfrom, 每次接收10个字节;
这个特点解决了报文的粘包问题。

UDP协议不可靠为什么还要用?
  • 不可靠不是说数据传不过去,只是如果数据丢包了,不关心。
  • 丢包:比如商家发快递,一个物品分成十份发送,用户收到后需要拼接,丢包率为20%时,客户只收到了8个包裹,如果协议是udp,就算其中几个包裹丢失,商家也不管,不会把丢失的那部分重新给你发。
  • 如果网络状态比较好,就不需要重新发送。
  • 简单直接说:丢包 ≠ 完全传不过去 ,它是数据传输过程中,部分数据包丢失、损坏、超时,没到达目的地的现象。
  • udp不可靠,无连接,说明udp简单,一些不需要关心数据丢失或超时的场景,使用udp可以提高效率,比如直播。
  • udp的不可靠,不能说成缺点,而是特点。
UDP的缓冲区
  • udp不需要保证可靠性,数据发送出去后,在本地直接就可以把数据删除。
  • UDP没有真正意义上的 发送缓冲区. 调⽤sendto会直接交给内核, 由内核将数据传给⽹络层协议进⾏后续的传输动作;
  • UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序⼀致 如果缓冲区满了, 再到达的UDP数据就会被丢弃
UDP使用的注意事项

我们注意到, UDP协议⾸部中有⼀个16位的最⼤⻓度. 也就是说⼀个UDP能传输的数据最⼤⻓度是
64K(包含UDP⾸部).
然⽽64K在当今的互联⽹环境下, 是⼀个⾮常⼩的数字.
如果我们需要传输的数据超过64K, 就需要在应⽤层⼿动的分包, 多次发送, 并在接收端⼿动拼装;

把网络进程,守护进程化

进程组
  • PGID就是进程组,PGID相同的进程就是同一组进程。

进程间关系与守护进程

进程组
  • 同一个进程组中的进程,进程pid最小的是组长。
  • 前后台进程管理是以进程组为单位的。
  • ctrl+c向发信号是向前台进程组发信号。
  • 同一组的进程可以使用隐藏管道通信。
作业
  • 进程组和作业是一个硬币的两面
  • 作业要由进程组来完成,作业和进程组可以是一回事。
  • 使用jobs查看作业数。
  • 将后台作业转为前台:fg [作业号]
会话
  • SID叫做会话ID
  • 我们每一次登录xshell就会在远程服务器上创建一个终端文件和bash,这两个加起来叫做会话。
进程如何成为一个独立的会话?
  • 哪个进程调用该函数,该进程就会独自成立一个会话
  • 创建独立会话的进程不能是进程组的组长。
  • 在命令行启动一个进程的作业,这个进程是自成组长的。
  • 创建子进程调用setsid,父进程退出,这个子进程就是守护进程,也是孤儿进程。
  • 进程组不等于组长进程,组长进程退出,进程组还存在,只有全部进程退出,组才会消失。

守护进程:

守护进程 = 升级版 + 彻底脱离终端 + 永久运行 的 后台进程

守护进程(Daemon)

  • 完全脱离终端、脱离用户登录会话
  • 系统开机自动运行,用户登出也不会死
  • 长期在后台默默服务(httpd、nginx、sshd、crontab)
  • 父进程变成 init/systemd(PID=1)

普通后台进程随终端,守护后台进程随系统。

创建守护进程

认识特殊文件dev/null

  • 打开zero文件,在这个文件中可以获取一些随机数据。
  • null文件类似回收站,写入这个文件中的内容,会被系统自动丢弃,读取这个文件,读到的都是0.
cpp 复制代码
#pragma once

#include <iostream>
#include<signal.h>
#include <unistd.h>
#include <fcntl.h>

void Daemon()
{
    //1.忽略信号
    signal(SIGPIPE, SIG_IGN);//忽略SIGPIPE信号,防止写入一个已经关闭的socket时,程序被杀死
    signal(SIGCHLD, SIG_IGN);//忽略SIGCHLD信号,防止子进程退出时产生僵尸进程

    //2不能是组长
    if(fork()>0)
    {
        exit(0);
    }

    //3.父进程退出(组长退出),子进程运行,设置新会话
    pid_t id = setsid();
    (void)id;

    //更改守护进程的工作目录
    chdir("/");

    //4.重定向0,1,2
    int fd = open("/dev/null", O_RDWR);
    if(fd>=0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        if(fd>2)
        {
            close(fd);
        }
    }
}
  • 系统调用的守护进程
  • 参数为是否要更改工作目录,是否要重定向,(0,0)更改工作目录并且重定向
相关推荐
zzzsde5 小时前
【Linux】库的制作和使用(3)ELF&&动态链接
linux·运维·服务器
CQU_JIAKE5 小时前
4.3【A]
linux·运维·服务器
RopenYuan5 小时前
FastAPI -API Router的应用
前端·网络·python
qing222222225 小时前
Linux中修改mysql数据表
linux·运维·mysql
@insist1235 小时前
网络工程师-VLAN 技术原理与配置指南(软考局域网核心考点)
网络·网络工程师·软考·软件水平考试
Alvin千里无风5 小时前
在 Ubuntu 上从源码安装 Nanobot:轻量级 AI 助手完整指南
linux·人工智能·ubuntu
TechWayfarer5 小时前
如何搭建企业级IP归属地查询平台?
网络·网络协议·tcp/ip
TechWayfarer5 小时前
科普:IP归属地中的IDC/机房/家庭宽带有什么区别?
服务器·网络·tcp/ip
杨云龙UP6 小时前
Oracle 中 NOMOUNT、MOUNT、OPEN 怎么理解? 在不同场景下如何操作?_20260402
linux·运维·数据库·oracle