Linux从青铜到王者第十五篇:Linux网络编程套接字两万字详解

Posted 森明帮大于黑虎帮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux从青铜到王者第十五篇:Linux网络编程套接字两万字详解相关的知识,希望对你有一定的参考价值。

系列文章目录



前言


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

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接口

(c)2006-2024 SYSTEM All Rights Reserved IT常识