Linux网络套接字编程

Posted 蓝乐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux网络套接字编程相关的知识,希望对你有一定的参考价值。

网络编程socket

💡预备知识

▶源IP地址与目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。这两个IP地址表述了这个数据包是从哪里来的,并且要到那里去,并且这两个地址表示的是数据包最初出发的地址,以及最终到达的地址;而实际上数据包在传输过程中并不止有这两个地址,还会有许多中间站,而用于表示数据包的上一站和下一站是源MAC地址和目的MAC地址。

▶端口号与进程id

端口号(port)是传输层协议的内容:

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用; 但一个进程可以绑定多个端口号。

▶源端口号与目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号。与源IP地址和目的IP地址类似,标识“数据是谁发的, 要发给谁”。

▶UDP与TCP协议

此处我们对udp和tcp协议有个直观的认识,后面详细讨论。

  • UDP协议(User Datagram Protocol 用户数据报协议)
    udp协议的特点是:面向数据报、无连接、不可靠的传输层协议。
  • TCP协议(Transmission Control Protocol 传输控制协议)
    tcp协议的特点是:面向字节流、有连接、可靠的传输层协议。

▶网络字节序

我们知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
先回顾一下何为大小端,对于一个整型,若高位在高地址处,低位在低地址处则为小端字节序;反之,若高位在低地址处,而低位在高地址处,则为大端字节序。

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
    TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
    为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:

    这些函数名比较好记,比如h表示主机(host),n表示网络(network),l表示32位长整型(long int),s表示16位短整型(short int)。htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
    如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

💡套接字(socket)介绍

▶概念

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。

▶分类

socket主要有以下三种类型:

  • 1.数据报套接字(SOCK_DGRAM
    数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。
  • 2.流套接字(SOCK_STREAM
    流套接字用于提供面向连接可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议。
  • 3.原始套接字(SOCK_RAM
    原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接字。

▶工作流程

要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket
对于面向连接的协议,套接字之间的连接过程可以分为三个步骤:

  • 1.服务器监听
  • 2.客户端请求
  • 3.确认连接

💡socket编程

由于socket编程的模式比较套路化,基本可以根据模板写出,因此,先将socket套接字编程熟练,可以更好的理解后续的udp/tcp协议及网络传输原理。

▶socket编程接口

// 创建 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);

▶sockaddr结构体

socket的API是一层抽象的网络编程接口,适用于各种底层网络协议,比如IPv4、IPv6,以及后续的Unix Domain Socket。然而各种网络协议的地址格式并不相同,因此我们需要用一个sockaddr结构体来描述对应的网络协议,区分地址类型,并且描述其端口号与IP地址。

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。
  • IPv4、IPv6地址类型分别定义为常数AF_INETAF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr*; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
  • 这里为什么不用void来强转的原因是最初网络提出socket套接字编程时还没有void类型,因此都是直接强制类型转换成sockaddr来使用接口的,而如果要替换为void,那么成本太大了。

🎄sockaddr结构体


前者为16位的地址类型,后14字节位地址路径。

🎄sockaddr_in结构体


虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构体中出了16位的地址类型,还包含端口号和IP地址,以及8字节填充内容。

🎄in_addr结构体


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

💡socket编程应用

▶基于udp协议的socket编程

由于udp协议是无连接的,因此udp的server与client不需要构建连接,直接客户端发出请求,服务器收到请求并处理。

🎄服务端server_udp

//udp server.cc
#include <iostream>
#include <string>
#include <strings.h>
#include <unistd.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using std::cerr;
using std::cout;
using std::endl;

void Usage(std::string proc)

    cout << "Usage:\\n\\t" << proc << "port" << endl;


// ./server 8080
// proc port
int main(int argc, char* argv[])

    if(argc != 2)
    
        Usage(argv[0]);
        exit(1);
    

    //1.创建套接字文件描述符
    int sock = socket(AF_INET, SOCK_DGRAM, 0);//udp: IPv4, 数据报

    //2.描述套接字端口号,IP地址信息
    struct sockaddr_in local;
    bzero(&local, sizeof(local));//初始化为0
    local.sin_family = AF_INET;//16位地址类型
    local.sin_port = htons(atoi(argv[1]));//16位端口号,主机转网络
    local.sin_addr.s_addr = htons(INADDR_ANY);//IP地址使用INADDR_ANY,表示不绑定具体的IP地址,这样可以bind机器上的所有IP

    //3.bind socket与sockaddr_in结构体
    if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    
        cerr << "bind error" << endl;
        return 2;
    

    //4.启动服务,收发消息
    char buf[1024];
    while(true)
    
        buf[0] = 0;
        sockaddr_in peer;//对端套接字
        socklen_t len = sizeof(peer);
        //从对端读取数据
        ssize_t s = recvfrom(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer, &len);
        if(s > 0)
        
            //success
            buf[s] = 0;
            cout << "client#" << buf << endl;//打印对端发来的数据
            std::string echo_message = buf;
            echo_message += " server received";
            //向对端发送数据
            sendto(sock, echo_message.c_str(), echo_message.size(), 0, (struct sockaddr*)&peer, len);
        
    
    close(sock);
    return 0;

这里需要注意,udp的服务端需要主动bind端口号与ip,但是这里bind的ip最好不要是具体的ip,因为一旦服务器bind的ip被占用,服务器就挂了,因此bind使用INADDR_ANY,可以bind云服务器的所有ip。
其次,这里介绍一下recvfrom接口:

🎄客户端client_udp

//udp client.cc
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using std::cout;
using std::cerr;
using std::endl;

void Usage(std::string proc)

    cout << "Usage\\n\\t" << proc << "dest_ip" << "dest_port" << endl;


// ./client 127.0.0.1 8080
// proc IP port
int main(int argc, char* argv[])

    if(argc != 3)
    
        Usage(argv[0]);
        return 1;
    

    int sock = socket(AF_INET, SOCK_DGRAM, 0);//创建客户端套接字

    //client不需要我们自己去bind,实际上在sendto的时候,操作系统会自动随机给client bind端口号

    //绑定端口号和ip 确定具体某台主机上的某个进程,描述服务器端的ip和端口号
    struct sockaddr_in desc;
    bzero(&desc, sizeof(desc));
    desc.sin_family = AF_INET;
    desc.sin_port = htons(atoi(argv[2]));//绑定端口号
    //用户端需要bind具体的ip地址,这样才能够连接到对应的服务器端
    //ip本质上可以由4个字节保存 127.0.0.1 点分十进制,这是我们习惯的写法
    //但对于计算机而言,更希望看到的是32位的整型ip地址
    desc.sin_addr.s_addr = inet_addr(argv[1]);//inet_addr函数作用就是将点分十进制的ip地址转换为无符号的长整型

    char buf[1024];

    while(true)
    
        buf[0] = 0;
        cout << "Please Enter#";
        fflush(stdout);
        ssize_t s = read(0, buf, sizeof(buf) - 1);
        if(s > 0)
        
            buf[s - 1] = 0;// 将\\n吸收
            //向对端发送数据,udp是无连接的,因此客户端直接向服务端发送请求
            sendto(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&desc/*向哪里发送*/, sizeof(desc)/*发送的长度*/);

            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            //从对端接收数据
            ssize_t size = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr*)&peer, &len);//peer与len暂且不用,仅作为接收使用
            buf[size] = 0;
            cout << buf << endl;
        
    
    close(sock);
    return 0;

客户端需要注意的点有:

  • 由于IP是点分十进制的传参,因此需要调用inet_ntoa转换为32为长整型。

🎄地址转换函数

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

  • 客户端无需主动bind端口号与ip,在向服务端发起请求调用sendto函数时OS会自动随机的为client去bind端口号与ip。这是因为不会有其他主机端口号来连接客户端,因此客户端无需自己有具体的ip与port,相反如果我们主动bind的话,还可能会bind失败。

🎄结果展示

这里bind的ip为127.0.0.1,表示本地环回,即数据包绕本地一圈后到回来,用于测试udp协议的实现,可以看见,client向server发送的数据都被接收到并返回应答。

▶基于tcp协议的socket编程

这里我们实现一个简易字典的服务器功能。

🎄服务端tcp_server

//tcp_server.hpp
#pragma once

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <string>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using std::cout;
using std::cerr;
using std::endl;

namespace ns_TcpServer

    typedef void(*handler_t)(int);

    const int backlog = 5;
    class TcpServer
    
    private:
        uint16_t port;
        int listen_sock;
    public:
        TcpServer(uint16_t _port)
        :port(_port)
        ,listen_sock(-1)
        
        void InitTcpServer()
        
            listen_sock = socket(AF_INET, SOCK_STREAM, 0);//tcp: IPv4 流套接字
            if(listen_sock < 0)
            
                cerr << "socket error" << endl;
                exit(2);
            

            sockaddr_in local;
            bzero(&local, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(port);//端口号 主机转网络 短整型
            local.sin_addr.s_addr = htonl(INADDR_ANY);//ip地址,使用INADDR_ANY,可以bind 机器上任意一个ip

            //bind 套接字 端口号与ip
            if(bind(listen_sock, (sockaddr*)&local, sizeof(local)) < 0)
            
                cerr << "bind error" << endl;
                exit(3);
            

            //监听,tcp是面向连接的,服务器监听等待客户端来发起连接
            if(listen(listen_sock, backlog) < 0)
            
                cerr << "listen error" << endl;
                exit(4);
            
        
        void Loop(handler_t handler)
        
            //启动服务
            while(true)
            
                sockaddr_in peer;
                socklen_t len = sizeof(peer);
                
                //建立好连接后,获取对端发来的消息
                int sock = accept(listen_sock, (sockaddr*)&peer, &len);
                if(sock < 0)
                
                    cout << "warning:accept error" << endl;
                    continue;
                

                handler(sock);//获取请求后回调函数处理请求

            
              

        ~TcpServer()if(listen_sock >= 0) close(listen_sock);
    ;

🌕tcp是面向连接的–listen_sock介绍

由于tcp协议是面向连接的,因此在双方进行通信之前,需要先建立连接,即server需要监听来自client的连接请求,因此在listen之前的套接字为监听套接字,而建立了连接之后用于获取对端消息的套接字才是和udp中作用一样的套接字。如何理解呢?
可以这么说,我们日常去饭店吃饭时,门口会站着揽客的服务员,而这些揽客的服务员拉到客人后,就交由饭店内的服务员来招待;而这里的listen_sock就类比作门口揽客的服务员,连接建立好后的sock就是招待的服务员。

//server.cc
#include "tcp_server.hpp"//提供网络连接功能
#include "handler.hpp"//提供处理网络套接字的功能

void Usage(std::string proc)

    cout << "Usage:\\n\\t" << proc << " port" << endl;


// ./tcp_server 8080
// proc port
int main(int argc, char* argv[])

    if(argc != 2)
    
        Usage(argv[0]);
    

    uint16_t port = atoi(argv[1]);

    ns_TcpServer::TcpServer* svr = new ns_TcpServer::TcpServer(port);

    svr->InitTcpServer();

    svr->Loop(ns_handler::Handler_V1);//单执行流

    return 0;

🎄客户端tcp_client

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


using std::cout;
using std::cerr;
using std::endl;

namespace ns_TcpClient

    class TcpClient
    
    private:
        std::string dest_ip;
        uint16_t dest_port;
        int sock;
    public:
        TcpClient(std::string _ip, uint16_t _port)
        :dest_ip(_ip)
        ,dest_port(_port)
        ,sock(-1)
        
        void InitClient(以上是关于Linux网络套接字编程的主要内容,如果未能解决你的问题,请参考以下文章

《网络编程与协议分析》复习题答案

Linux/UNIX网络编程的目录

Linux 网络编程套接字

linux网络编程——套接字(socket)入门

Linux网络编程和套接字

Linux 网络编程 --套接字编程