linux从青铜到王者第十五篇:linux网络编程套接字两万字详解(代码片段)

森明帮大于黑虎帮 森明帮大于黑虎帮     2022-12-16     157

关键词:

系列文章目录



前言


一、网络数据的五元组信息

1.理解源IP地址和目的IP地址

  • 在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址:
  • 源IP地址:表示该条信息来源于哪个机器。
  • 目的IP地址:表示该条信息去往于哪个进程。

2.理解 “端口号” 和 “进程ID”

  • 端口号(port)是传输层协议的内容:
  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
  • 一个端口号只能被一个进程占用。
  • 一个进程可以绑定多个端口号。
  • pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程。

3.理解源端口号和目的端口号

  • 传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”:
  • 源端口号:表示该条信息来源于哪个进程。
  • 目的端口号:表示该条信息去往于哪个机器。

以寄快递为例子:

4.理解TCP协议

  • 协议:两台机器传输时用哪种协议。
  • TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题。
  • 传输层协议。
  • 有连接:双方在发送网络数据之前必须建立连接,在进行发送。
  • 可靠传输:保证数据是可靠并且有序的到达对端,例如发送123、456时123数据先到达,456数据后到达,但是有时可以456数据先到达传输层,但会阻塞等待先等前面的数据就是123先到达。
  • 面向字节流:TCP发送数据的单位是以字节为单位,并且数据没有明显的边界例如:123456数据不会分开。

    假设应用层要想传输层传入“hello”,当hello传入传输层还尾传入网络层时,应用层又想向传输层传入“world”,此时是不能传输的,只有等“hello”从传输层传入网络层,“world”才能从应用层传入传输层:

5.理解UDP协议

  • 此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论。
  • 传输层协议。
  • 无连接:双方在发送网络数据之前不需要建立连接,直接发送,客服端不用管服务端是否在线。
  • 可靠传输:UDP并不会保证数据有序的到达对端
  • 面向字节流:UDP不管向应用层还是网络层传递数据都是整条数据

假设A机器的应用层先向传输层传入一个“aaa”,再向传输层传入一个“bbb”,到待对端机器的传输层不会区分,是不是一次传过来的:

二、主机字节序<===>网络字节序

  • 我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
  • 网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
  • 网络数据需要进行转发之前:由主机字节序转换成为网络字节序。
  • 网络数据接收之前:由网络字节序转换成为主机字节序。
  • 【问题】为什么网络数据需要进行转化成为网络字节序?
  • 网络规定采用大端字节序作为网络字节序。
  • 路由设备或者交换机需要对网络数据进行分用到网络层面,以获取到“目的IP地址”,而这些设备在进行分用的时候默认是按照网络字节序进行分用的。
  • 主机字节序转换为网络字节序(host to network)
  • 2个字节  uint16_t htons(uint16_t hostshort)。
  • 4个字节  uint32_t htonl(uint32_t hostlong)。
  • 网络字节序转换为主机字节序( to network)
  • 2个字节  uint16_t ntohs(uint16_t netshort);
  • 4个字节  uint32_t ntohl(uint32_t netlong);

三、点分十进制IP<===>uint32_t


本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;字符串转in_addr的函数。

  • 点分十进制IP转换成为uint32_t
  • in_addr_t inet_addr(const char * cp);
  • 将字符串的点分十进制IP地址转换为uint32_t
  • 将uint32_t从主机字节序转换成为网络字节序。
  • uint32_t转换成为点分十进制IP
  • char * inet_ntoa(struct in_addr in);
  • 将网络字节序uint32_t的整数转换成为主机字节序。
  • 将uint32_t转换成为点分十进制的字符串。

四、UDP的socket编程(流程&接口)

1.UDP的socket编程流程

  • cs模型(客户端服务端):client-server。
  • bs模型:浏览器-服务器。

1.socket常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr * address,socklen_t address_len);*
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr*addr,socklen_t addrlen);

2.socketaddr结构的分类

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。
  • IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API可以都用struct sockaddr * 类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。

3.socketaddr结构

4.socketaddr_in结构


虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。

5.in_addr结构


in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。

2.UDP的socket编程接口

1.创建套接字socket接口

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

     1	#include<iostream>
     2	#include<sys/types.h>
     3	#include<sys/socket.h>
     4	#include<unistd.h>
     5	
     6	using namespace std;
     7	int main()
     8	
     9	    int SockFd=socket(AF_INET,SOCK_STREAM,0);
    10	    if(SockFd<0)
    11	    
    12	        cout<<"套接字创建失败!"<<endl;
    13	    
    14	    cout<<"SockFd:"<<SockFd<<endl;
    15	    while(1)
    16	    
    17	        sleep(1);
    18	    
    19	    return 0;
    20	

2.绑定端口号bind接口

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr * address,socklen_t address_len);*

  • sockfd:socket函数返回的套接字描述符;将创建出来的套接字和网卡,端口好进行绑定
  • addr:地址信息结构
  • addr的类型是struct sockaddr ,struct sockaddr 是一个通用地址信息结构,如下图所示:

    假设,定义一个int fun(void * x)参数可以接收任何类型数据的函数,使用时就需要强转 char* p = “abc”;   fun((void*)lp);而如上结构体的作用相当于此例中的参数,因为bind函数可能绑定 ipv4(uint32_t) / ipv6(uint128_t) / 本地域套接字 等不同类型的协议,所以绑定不同版本的IP地址需要提供不同的绑定函数,而此做法非常的麻烦,所以将协议的数据结构定义为一个通用的,要使用某一具体的协议,只需传入具体的协议对应的数据结构并强转即可。
    如下图所示,我们可以在 vim /usr/include/netinet/in.h 路径下查看ipv4协议使用的结构体:
  • addrlen:地址信息结构的长度(告诉网络协议栈最多能解析多少个字节)
     1	#include<iostream>
     2	#include<sys/types.h>
     3	#include<sys/socket.h>
     4	#include<unistd.h>
     5	#include<netinet/in.h>
     6	#include<arpa/inet.h>
     7	using namespace std;
     8	
     9	int main()
    10	
    11	    int SockFd=socket(AF_INET,SOCK_DGRAM,0);
    12	    if(SockFd<0)
    13	    
    14	        cout<<"套接字创建失败!"<<endl;
    15	    
    16	    cout<<"SockFd:"<<SockFd<<endl;
    17	
    18	    struct sockaddr_in addr; 
    19	
    20	    addr.sin_port=htons(20000);
    21	    addr.sin_family=AF_INET;
    22	    addr.sin_addr.s_addr=inet_addr("172.16.0.9");
    23	    int ret=bind(SockFd,(struct sockaddr*)&addr,sizeof(addr));
    24	    if(ret<0)
    25	    
    26	        cout<<"绑定失败!"<<endl;
    27	        return 0;
    28	    
    29	    while(1)
    30	    
    31	        sleep(1);
    32	    
    33	    return 0;
    34	

3.UDP发送接口sendto

ssize_t sendto(int sockfd, const void * buf, size_t len, int flags,const struct sockaddr * dest_addr, socklen_t addrlen);

  • sockfd:套接字描述符
  • buf:要发送的数据
  • len:发送数据的长度
  • flags:0 阻塞发送
  • dest_addr:目标主机的地址信息结构(IP,port)
  • addrlen:目标主机地址信息结构的长度
  • 返回值:
    成功返回具体发送的字节数量,失败返回-1

4.UDP接收接口recvform

ssize_t recvfrom(int sockfd, void * buf, size_t len, int flags,struct sockaddr * src_addr, socklen_t * addrlen);

  • sockfd:套接字描述符
  • buf:将数据接收到buf当中
  • len:buf的最大接收能力
  • flags:0阻塞接收
  • src_addr:这个数据来源的主机的地址信息结构(IP,port)---->由recvfrom()函数填充
  • addrlen:输入输出型参数
    输入:在接收之前准备的对端地址信息结构的长度
    输出:实际接收回来的地址信息长度

5.UDP关闭接口close

close(int sockfd);

3.客户端为什么不推荐绑定地址信息

本质上是不想让客户端程序将端口写死,即不想让客户端在启动的时候,都是绑定一个端口的(一个端口只能被一个进程所绑定)。

eg:客户端A绑定了端口,本机在启动客户端B的时候就会绑定失败
  当客户端没有主动的绑定端口,UDP客户端在调用sendto的时候,会自动绑定一个空闲的端口(操作系统分配一个空闲的端口)。

五、UDP的socket编程代码

1.客户端

客户端只需创建套接字,向服务端发送请求,接收服务端的回复即可。

     1	#include<iostream>
     2	#include<stdio.h>
     3	#include<sys/types.h>
     4	#include<sys/socket.h>
     5	#include<unistd.h>
     6	#include<netinet/in.h>
     7	#include<arpa/inet.h>
     8	#include<string.h>
     9	#include<stdlib.h>
    10	using namespace std;
    11	
    12	int main()
    13	
    14	    int SockFd=socket(AF_INET,SOCK_DGRAM,0);
    15	    if(SockFd<0)
    16	    
    17	        cout<<"套接字创建失败!"<<endl;
    18	    
    19	    cout<<"SockFd:"<<SockFd<<endl;
    20	
    21	   /* struct sockaddr_in addr; 
    22	
    23	    addr.sin_port=htons(20000);
    24	    addr.sin_family=AF_INET;
    25	    addr.sin_addr.s_addr=inet_addr("172.16.0.9");
    26	    int ret=bind(SockFd,(struct sockaddr*)&addr,sizeof(addr));
    27	    if(ret<0)
    28	    
    29	        cout<<"绑定失败!"<<endl;
    30	        return 0;
    31	    */
    32	    while(1)
    33	    
    34	        char buf[1024]="i am client!";
    35	        struct sockaddr_in dest_addr;
    36	        dest_addr.sin_family=AF_INET;
    37	        dest_addr.sin_port=htons(20000);
    38	        dest_addr.sin_addr.s_addr=inet_addr("1.14.165.138");
    39	
    40	        sendto(SockFd,buf,strlen(buf),0,(struct sockaddr*)&dest_addr,sizeof(dest_addr));
    41	
    42	        memset(buf,'\\0',sizeof(buf));
    43	
    44	        struct sockaddr_in peer_addr;
    45	        socklen_t len=sizeof(peer_addr);
    46	
    47	        ssize_t rece_size=recvfrom(SockFd,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer_addr,&len);
    48	        if(rece_size<0)
    49	        
    50	            continue;
    51	        
    52	        cout<<"recv msg:"<<buf<<" from "<<inet_ntoa(peer_addr.sin_addr)<<" "<<ntohs(peer_addr.sin_port)<<endl;
    53	        sleep(1);
    54	    
    55	    close(SockFd);
    56	    return 0;
    57	

2.服务端

服务端只需创建套接字,绑定端口,接收客户端的请求,回复客户端信息即可。

     1	#include<iostream>
     2	#include<stdio.h>
     3	#include<sys/types.h>
     4	#include<sys/socket.h>
     5	#include<unistd.h>
     6	#include<netinet/in.h>
     7	#include<arpa/inet.h>
     8	#include<string.h>
     9	#include<stdlib.h>
    10	using namespace std;
    11	
    12	int main()
    13	
    14	    int SockFd=socket(AF_INET,SOCK_DGRAM,0);
    15	    if(SockFd<0)
    16	    
    17	        cout<<"套接字创建失败!"<<endl;
    18	    
    19	    cout<<"SockFd:"<<SockFd<<endl;
    20	
    21	    struct sockaddr_in addr; 
    22	
    23	    addr.sin_port=htons(20000);
    24	    addr.sin_family=AF_INET;
    25	    addr.sin_addr.s_addr=inet_addr("172.16.0.9");
    26	    int ret=bind(SockFd,(struct sockaddr*)&addr,sizeof(addr));
    27	    if(ret<0)
    28	    
    29	        cout<<"绑定失败!"<<endl;
    30	        return 0;
    31	    
    32	    while(1)
    33	    
    34	        char buf[1024]=0;
    35	        struct sockaddr_in peer_addr;
    36	        socklen_t len=sizeof(peer_addr);
    37	        ssize_t rece_size=recvfrom(SockFd,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer_addr,&len);
    38	        if(rece_size<0)
    39	        
    40	            continue;
    41	        
    42	        cout<<"recv msg:"<<buf<<" from "<<inet_ntoa(peer_addr.sin_addr)<<" "<<ntohs(peer_addr.sin_port)<<endl;
    43	
    44	        memset(buf,'\\0',sizeof(buf));
    45	        sprintf(buf,"welcome client %s:%d\\n",inet_ntoa(peer_addr.sin_addr),ntohs(peer_addr.sin_port));
    46	        sendto(SockFd,buf,strlen(buf),0,(struct sockaddr*)&peer_addr,sizeof(peer_addr));
    47	    
    48	    close(SockFd);
    49	    return 0;
    50	

3.查看端口的使用情况:netstat -anp | grep [端口号]

六、TCP的socket编程(流程&接口)

1.TCP的socket编程流程

2.TCP的socket编程接口

创建套接字接口socket(),绑定端口bind(),关闭套接字接口close(),的使用和UDP套接字编程中的使用是一样的,下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。

1.服务端创建套接字socket接口

  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符。
  • 应用程序可以像读写文件一样用read/write在网络上收发数据。
  • 如果socket()调用

    linux从青铜到王者第十四篇:linux网络基础第一篇

    系列文章目录文章目录系列文章目录前言一、计算机网络的发展过程1.独立模式2.网络互联模式3.局域网LAN4.广域网WAN二、认识计算机网络协议1.协议的概念2.什么是网络协议3.网络协议簇4.体系结构5.OSI七层模型6.TCP/IP五层(或四层)... 查看详情

    linux从青铜到王者第十八篇:linux网络基础第二篇之tcp协议

    系列文章目录文章目录系列文章目录前言一、TCP面向字节流二、TCP粘包问题1.什么是TCP粘包问题2.TCP粘包问题的解决办法三、TCP异常情况四、TCP协议1.TCP协议段格式2.确认应答(ACK)机制3.超时重传机制4.连接管理机制1、TCP三次握手1.... 查看详情

    linux从青铜到王者第十七篇:linux网络基础第二篇之udp协议

    系列文章目录文章目录系列文章目录前言一、传输层1.再谈端口号2.端口号范围划分3.认识知名端口号(Well-KnowPortNumber)4.进程和端口号两个问题5.netstat查看网络状态二、UDP协议1.UDP协议端格式2.UDP的特点3.面向数据报4.UDP的缓冲区5.UDP... 查看详情

    c++从青铜到王者第二十五篇:c++智能指针(代码片段)

    系列文章目录文章目录系列文章目录前言一、常见面试题1.malloc/free和new/delete的区别2.内存泄漏1.内存泄漏概念与危害2.内存泄漏分类(了解)3.如何检测内存泄漏(了解)4.如何避免内存泄漏5.如何一次在堆上申请4G... 查看详情

    linux从青铜到王者第十六篇:linux网络基础第二篇之http协议(代码片段)

    系列文章目录文章目录系列文章目录前言一、HTTP协议的概念二、HTTP协议URL的解释三、HTTP协议的数据流四、HTTP协议格式1.HTTP请求2.HTTP响应五、HTTP协议格式图解六、HTTP协议版本七、HTTP协议请求方法1.GET:获取资源2.POST:... 查看详情

    linux从青铜到王者第十二篇:linux进程间信号第二篇(代码片段)

    系列文章目录文章目录系列文章目录前言一、阻塞信号1.信号其他相关常见概念2.在内核中的表示3.sigset_t信号集4.信号集操作函数5.sigprocmask函数6.sigpending函数二、捕捉信号1.内核实现信号的捕捉2.volatile关键字总结前言一、阻塞信... 查看详情

    linux从青铜到王者第十九篇:linux网络基础第二篇之滑动窗口流量控制拥塞控制延迟应答捎带应答

    系列文章目录文章目录系列文章目录前言👮一、滑动窗口💰一、滑动窗口的由来💰二、滑动窗口存在的问题💷1.滑动窗口的大小💷2.数据包已经传输给对方,但是对方返回的ACK数据包丢失💷3.传输的... 查看详情

    linux从青铜到王者第十三篇:linux多线程四万字详解(代码片段)

    系列文章目录文章目录系列文章目录前言一、Linux线程概念1.什么是线程2.线程的优点3.线程的缺点4.线程的异常5.线程的用途二、进程和线程的对比1.进程和线程2.多进程的应用场景有哪些?三、线程控制1.POSIX线程库2.创建线程... 查看详情

    linux从青铜到王者第二十篇:linux网络基础第三篇之ip协议

    系列文章目录文章目录系列文章目录前言一、IP协议基本概念二、IPv4首部三、网络号和主机号四、早期地址管理方式五、CIDR(ClasslessInterdomainRouting)方式六、特殊的IP地址七、IP地址的数量限制八、路由控制总结前言一、IP协议基本... 查看详情

    linux从青铜到王者第二十四篇:linux网络基础第四篇之websocket协议(代码片段)

    系列文章目录文章目录系列文章目录前言一、WebSocket简介二、WebSocket产生背景三、WebSocket实现原理四、WebSocket协议举例五、WebSocket使用1.WebSocket介绍2.WebSocketAPI3.WebSocket事件1.open2.Message3.Error4.Close4.WebSocket方法1.send()2.close()5.WebSocket... 查看详情

    mysql从青铜到王者第五篇:mysql内置函数

    系列文章目录文章目录系列文章目录前言一、日期函数1.获得年月日2.获得时分秒3.获得时间戳4.在日期的基础上加上时间5.在日期的基础上减去时间6.计算两个日期相差多少天7.当前日期时间date(now())8.date(datetime)返回datetime的参数... 查看详情

    linux从青铜到王者第二十一篇:linux网络基础第三篇之数据链路层

    系列文章目录文章目录系列文章目录前言一、数据链路层的以太网协议二、认识MAC地址三、对比理解MAC地址和IP地址四、ARP协议的作用五、ARP数据报格式五、ARP协议工作流程六、ARP缓存表七、DNS(DomainNameSystem)八、NAT协议九、NAPT协... 查看详情

    c++从入门到入土第十五篇:list的模拟实现

    查看详情

    c语言从青铜到王者第五篇·数据在内存中的存储(代码片段)

    本篇前言从本篇开始,我们要开始逐渐和内存打交道了。想学好C语言,打牢编程基本功,我们心中一定要时刻有内存的概念。文章目录数据类型及其意义整型与浮点型构造(自定义)类型空类型指针类型数据... 查看详情

    从青铜到王者,你只差这篇全网最全linux命令大全

    〝古人学问遗无力,少壮功夫老始成〞从青铜到王者,你只差这篇全网最全linux命令大全。全网最全linux命令大全,从入门到精通,助力大厂橄榄枝,如果这篇文章能给你带来一点帮助,希望给飞兔小哥哥... 查看详情

    从青铜到王者,你只差这篇全网最全linux命令大全

    〝古人学问遗无力,少壮功夫老始成〞从青铜到王者,你只差这篇全网最全linux命令大全。全网最全linux命令大全,从入门到精通,助力大厂橄榄枝,如果这篇文章能给你带来一点帮助,希望给飞兔小哥哥... 查看详情

    设计模式从青铜到王者第五篇:创建型模式之简单工厂模式(simplefactorypattern)(代码片段)

    系列文章目录文章目录系列文章目录前言一、简单工厂模式模式动机二、简单工厂模式模式定义三、简单工厂模式模式结构四、简单工厂模式时序图五、简单工厂模式代码分析六、简单工厂模式模式分析七、简单工厂模式优点八... 查看详情

    linux从青铜到王者第二十二篇:linux高级io(代码片段)

    系列文章目录文章目录系列文章目录前言一、五种IO模型1.阻塞IO2.非阻塞IO3.信号驱动IO4.异步IO5.IO多路转接二、高级IO重要概念1.同步通信vs异步通信2.阻塞vs非阻塞三、I/O多路转接之select1.select函数的作用2.select函数的原型3.fd_set结... 查看详情