您现在的位置是:首页 >技术杂谈 >网络编程套接字网站首页技术杂谈

网络编程套接字

想变成自大狂 2024-06-17 11:27:59
简介网络编程套接字

目录

TCP 网络编程架构

创建网络插口函数socket()

绑定一个地址端口对bind()函数 

监听本地端口listen

接受一个网络请求accept()函数

连接目标网络服务器connect()函数

写入数据函数 wite()

读取数据函数read()

 关闭套接字函数


TCP网络编程是目前比较通用的方式,例如HTTP、FTP协议等很多广泛应用的协议均基于TCP协议。TCP编程主要为C/S模式,即客户端(C)、 服务器(S)模式,这两种模式之间的程序设计流程存在很大的差别。

TCP 网络编程架构

TCP网络编程有两种模式、一种是服务器模式,另一种是客户端模式。服务器模式创建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后,根据用户的请求进行处理:客严霜楼式则根据目的服务器的地址和端口进行连接,向服务器发送请求并对服务器的响应进行数据处理。
 

服务器端的程序设计模式

如图所示为TCP连接的服务器模式的程序设计流程。流程主要分为套接字初始化(socket()函数),套接字与端口的绑定(bind()函数),设置服务器的侦听连接( listen()函数),接受客户端连接(accept()函数),接收和发送数据(read()函数、write()函数)并进行数据处理及处理完毕的套接字关闭(close()函数)。

  • 套接字初始化过程中,根据用户对套接字的需求来确定套接字的选项。这个过程中的函数为socket(),它按照用户定义的网络类型、协议类型和具体的协议标号等参数来定义。系统根据用户的需求生成一个套接字文件描述符供用户使用。
  • 套接字与端口的绑定过程中,将套接字与一个地址结构进行绑定。绑定之后,在进行网络程序设计的时候,套接字所代表的IP地址和端口地址,以及协议类型等参数按照绑定值进行操作。
  • 由于一个服务器需要满足多个客户端的连接请求,而服务器在某个时间仅能处理有限个数的客户端连接请求,所以服务器需要设置服务端排队队列的长度。服务器侦听连接会设置这个参数,限制客户端中等待服务器处理连接请求的队列长度。
  • 在客户端发送连接请求之后,服务器需要接收客户端的连接,然后才能进行其他的处理。
  • 在服务器接收客户端请求之后,可以从套接字文件描述符中读取数据或者向文件描述符发送数据。接收数据后服务器按照定义的规则对数据进行处理,并将结果发送给客户端。
  • 当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接。

客户端的程序设计模式
客户端模式,主要分为套接字初始化(socket()函数),连接服务器( connect()函数),读写网络数据(read()函数、write()函数)并进行数据处理和最后的套接字关闭(close()函数)过程。

客户端程序设计模式流程与服务器端的处理模式流程类似,二者的不同之处是客户项在套接字初始化之后可以不进行地址绑定,而是直接连接服务器端。
客户端连接服务器的处理过程中,客户端根据用户设置的服务器地址、端口等参数与特定的服务器程序进行通信。

客户端与服务器的交互过程 

客户端与服务器在连接、读写数据、关闭过程中有交互过程。

  • 客户端的连接过程,对服务器端是接收过程,在这个过程中客户端与服务进行三次握手,建立TCP连接。建立TCP连接之后,客户端与服务器之间可以进行数据的交互。
  • 客户端与服务器之间的数据交互是相对的过程,客户端的读数据过程对应了服务器端的写数据过程,客户端的写数据过程对应服务器的读数据过程。
  • 在服务器和客户端之间的数据交互完毕之后,关闭套接字连接。 

创建网络插口函数socket()

网络程序设计中的套接字系统调用socket()函数用来获得文件描述符。

#include <sys/types.h>         
 #include <sys/socket.h>

       int socket(int domain, int type, int protocol);

这个函数建立一一个协议族为domain、协议类型为type、 协议编号为protocol 的套接字文件描述符。如果函数调用成功,会返回一个表示这个套接字的文件描述符,失败的时候返回-1。

函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。通信协议族在文件sys/socket.h中定义,包含下表所示的值,以太网中应该使用PF_INET这个域。在程序设计的时候会发现有的代码使用了AF_INET这个值,在头文件中AF_INET和PF_INET的值是一致的。

 domain的含义以及值

名称含义名称含义
PF_UNIX,PF_LOCAL本地通信PF_X25ITU-T X.25 / ISO-8208协议
PF_INETIPv4 Intermet协议PF_AX25Amateur radio AX.25协议
PF_INET6IPv6 Internet协议PF_ATMPVC
 
原始ATM  PVC访问
PF_IPXIPX- Novell协议PF_APPLETALKAppletalk
PF_NETLINK内核用户界面设备PF_PACKET底层包访问

函数socket()的参数type用于设置套接字通信的类型,主要有SOCK_STREAM (流式套接字)、SOCK_DGRAM (数据包套接字)等。

type的值的含义

  • SOCK_STREAM:TCP连接,提供序列化的、 可称的、双向连接的字节流。支持带外数据传输。
  • SOCK_DGRAM:支持UDP连接(无连接状态的消息)。
  • SOCK_SEQPACKET:序列化包,提供一个序列化的、可靠的、双向的基于连接的数据传输通道,数据长度定长。每次调用读系统调用时数据需要将全部数据读出。
  • SOCK_RAW:RAW类型,提供原始网络协议访间。
  • SOCK_RDM:提供可靠的数据报文,不过数据可能会有乱序。
  • SOCK_PACKET:这是一个专用类型,不能在通用程序中使用。

函数socket()的第3个参数protocol用于指定某个协议的特定类型,即type类型中的某个类型。通常某个协议中只有一种特定类型, 这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。

  • 类型为SOCK_STREAM的套接字表示一个双向的字节流,与管道类似。流式的套接字在进行数据收发之前必须已经连接,连接使用connect()函数进行。 一旦连接,可以使用read()或者write()函数进行数据的传输。流式通信方式保证数据不会丢失或者重复接收,当数据在一段时间内仍然没有接收完毕,可以认为这个连接已经死掉。
  • SOCK_DGRAM和SOCK_RAW这两种套接字可以使用函数sendto()来发送数据,使用revfrom()函数接收数据,recvfrom()接收来自指定IP地址的发送方的数据。
  • SOCK_PACKET是一种专用的数据包,它直接从设备驱动接收数据。

函数socket()并不总是执行成功,有可能会出现错误,错误的产生有多种原因,可以通过errno获得。通常情况下造成函数socke()失败的原因是输入的参数错误造成的,例如某个协议不存在等,这时需要详细检查函数的输入参数。由于函数的调用不一定成功,在进行程序设计的时候,一定要检查返回值。

使用socket()函数的时候需要设置上述3个参数,如将socket()函数的第1个参数domain设置为AF_INET,第2个参数设置为SOCK_STREAM, 第3个参数设置为0,建立一个流式套接字。

int sock = socket(AF_INET, SOCK_STREAM, 0);

static int Socket()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if(sock < 0)
        {
            cerr << "socket error" << endl;
            exit(2);
        }
        return sock;
    }

绑定一个地址端口对bind()函数 

在建立套接字文件描述符成功后,需要对套接字进行地址和端口的绑定,才能进行数据的接受和发送操作。

 #include <sys/types.h> 
 #include <sys/socket.h>

       int bind(int sockfd, const struct sockaddr *addr,  socklen_t addrlen);

  • sockfd:是用socket()函数创建的文件描述符。
  • addr:是指向一个结构为sockaddr参数的指针 ,sockaddr 中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要先将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等结合在一起。
  • addrlen:是addr结构的长度,可以设置成sizeof(struct sockaddr)。使用sizeof(struct sockaddr)来设置addrlen是一个良好的习惯, 虽然般情况下使用AF_INET来设置套接字的类型和其对应的结构,但是不同类型的套接字有不同的地址描述结构,如果对地址长度进行了强制的指定,可能会造成不可预料的结果。
  • bind()函数的返回值为0时表示绑定成功,-1表示绑定失败。

sockaddr结构

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

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结构体指针作为参数。

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

static void Bind(int sock, uint16_t port)
    {
        struct sockaddr_in local;//以太网套接字地址结构
        memset(&local, 0, sizeof(local));//将变量local置0
        local.sin_family = AF_INET;
        local.sin_port = htons(port);//地址结构的端口地址,网络字节序,htons将主机字节序转化,
        local.sin_addr.s_addr = INADDR_ANY;

        if(bind(sock,(struct sockaddr*)&local, sizeof(local)) < 0)
        {
            cerr << "bind error" <<endl;
            exit(3);
        }
    }

监听本地端口listen

服务器模式中有listen()和accept()两个函数,而客户端则不需要这两个函数。函数listen()用来初始化服务器可连接队列,服务器处理客户端连接请求的时候是顺序处理的,同一时间仅能处理一个客户端连接。 当多个客户端的连接请求同时到来的时候,服务器并不是同时处理,而是将不能处理的客户端连接请达到等待队列中,这个队列的长度由listen()函数来定义。

listen()函数的原型如下,其中的backlog表示等待队列的长度。

#include <sys/socket.h>
int  listen(int sockfd, int backlog);

当listen()函数成功运行时,返回值为0;当运行失败时,它的返回值为-1,并且设置errno值,错误代码的含义:
listen()函数的 errno 值及含义

  • EADDRINUSE:另一个socket已经在同-端口侦听
  • EBADF:参数sockfd不是合法的描述符
  • ENOTSOCK:参数sockfd不是代表socket的文件描述符
  • EOPNOTSUPP:socket不支持listen操作

接受一个连接之前,需要用listen()函数来侦听端口,listen()函数中参数backlog的参数表示在accept()函数处理之前在等待队列中的客户端的长度,如果超过这个长度,客户端会返回一个ECONNREFUSED错误。

listen()函数仅对类型为SOCK_STREAM或者SOCK_SEOPACKET的协议有效,例如,如果对一个SOCK_DGRAM的协议使用函数listen(),将会出现errno应该值为EOPNOTSUPP,表示此socket不支持函数listen()操作。大多数系统的设置为20,可以将其设置修改为5或者10,根据系统可承受负载或者应用程序的需求来确定。

  static void Listen(int sock)
    {
        if(listen(sock, 5) < 0)
        {
            cerr << "listen error" <<endl;
            exit(4);
        }
    }

接受一个网络请求accept()函数
 

当一个客户端的连接请求到达服务器主机侦听的端口时,此时客户端的连接会在队列中等待,直到使用服务器处理接收请求。
函数accept()成功执行后,会返回一个新的套接字文件描述符来表示客户端的连接,客户端连接的信息可以通过这个新描述符来获得。因此当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符表示正在监听的socket, 新产生的文件描述符表示客户端的连接,函数send()和recv()通过新的文件描述符进行数据收发。 

#include <sys/types.h>     
#include <sys/socket.h>

       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

通过accept()函数可以得到成功连接客户端的IP地址、端口和协议族等信息,这个信息是通过参数addr获得的。当accept()函数返回的时候,会将客户端的信息存储在参数addr中。参数addrlen表示第2个参数( addr )所指内容的长度,可以使用sizeof(struct sockaddr_in)来获得。需要注意的是,在acept中addrlen参数是一个指针而不是结构,accept()函数将这个指针传给TCP/IP协议栈。
accpet()函数的返回值是新连接的客户端套接字文件描述符,与客户端之间的通信是通过accept()函数返回的新套接字文件描述符来进行的,而不是通过建立套接字时的文件描述符来进行的。这是在程序设计的时候需要注意的地方。如果accept()函数发生错误,accept()函数会返回-1。

static int Accept(int sock)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = accept(sock, (struct sockaddr*)&peer, &len);
        if(fd >= 0)
            return fd;
            
        return -1;
    }

连接目标网络服务器connect()函数

客户端在建立套接字之后,不需要进行地址绑定就可以直接连接服务器。连接服务器的函数为connect()此函数连接指定参数的服务器,例如IP地址、端口等。

#include <sys/types.h>  
#include <sys/socket.h>

       int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd是建立套接字时返回的套接字文件描述符,它是由系统调用socket()函数返回的。参数addr,是一个指向数据结构sockaddr的指针,其中包括客户端需要连接的服务器的目的端口和IP地址,以及协议类型。 参数addrlen表示第二个参数内容的大小,可以使用sizeof(struct sockaddr)而获得,与accept()函数不同,这个参数是一个整型的变量而不是指针。connect()函数的返回值在成功时为0,当发生错误的时候返回-1。

写入数据函数 wite()

服务器端在接收到一个客户端的连接后,可以通过套接字描述符进行数据的写入操作。对套接字进行写入的形式和过程与普通文件的操作方式一致,内核会根据文件描述符的值来查找所对应的属性,当为套接字的时候,会调用相对应的内核函数。

下面是个向套接字文件描述符中写入数据的例子,将缓冲区data的数据全锦写入套接字文件描述符s中,返回值为成功写入的数据长度。

int size;
char data[1024] ;
size = write(s, data, 1024) ;

读取数据函数read()

与写入数据类似,使用read()函数可以从套接字描述符中读取数据。当然,在读取数据之前,必须建立套接字并连接。读取数据的方式如下所示,从套接字描述符s中读取1024个字节,放入缓冲区data中,size 变量的值为成功读取的数据大小。

int size ;
char data[1024];
size = read(s, data, 1024);

 关闭套接字函数

关闭socket连接可以使用close()函数实现,函数的作用是关闭已经打开的socket连接,内核会释放相关的资源,关闭套接字之后就不能再使用这个套接字文件描述符进行读写操作了。

 函数shudown()可以使用更多方式来关闭连接,允许单方向切断通信或者切断双方的通信。

#include <sys/socket. h>
        int shutdown(int s, int how);

第一个参数s是切断通信的套接口文件描述符,第二个参数how麦示切断的方式。
函数shutdow()用于关闭双向连接的一部分, 具体的关团行为方式通过参数的how设置来实现。可以为如下值:

  • SHUT_RD:值为0,表示切断读,之后不能使用此文件描述符进行读操作。
  • SHUT_WR:值为1表示切断写,之后不能使用此文件描述符进行写操作。
  • SHUT_REWR:值为2:表示切断读写,之后不能使用此文件描述进行读写操作,与close()函数功能相同。

函数shutdow()如果调用成功则返回0,如果失败则返回-1。

服务器代码:

#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
std::string Usage(std::string proc)
{
    std::cout << "Usage: 
	" << proc << "port" << std::endl;
}

void ServiceIO(int new_sock)
{
    // 提供服务
        while (true)
        {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t s = read(new_sock, buffer, sizeof(buffer) - 1);
            if (s > 0)
            {
                buffer[s] = 0;//将获取的内容当成字符串
                std::cout << "client# " << buffer << std::endl;

                std::string echo_string = ">>>server<<<,";
                echo_string += buffer;

                write(new_sock, echo_string.c_str(), echo_string.size());
            }
            else if(s == 0)
            {
                std::cout << "client quit ..." << std::endl;
                break;
            }
            else
            {
                std::cerr << "read error" << std::endl;
                break;
            }
        }
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return 1;
    }
    // 1.创建套接字
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0); // SOCK_STREAM---tcp
    if (listen_sock < 0)
    {
        std::cerr << "socket error " << errno << std::endl;
        return 2;
    }

    // 2.bind:绑定
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1]));
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error " << errno << std::endl;
        return 3;
    }
    // 3.tcp是面向连接的,在通信前需要建立连接然后才能通信
    // 设置套接字是listen状态,本质是允许用户连接
    const int back_log = 5;
    if (listen(listen_sock, back_log) < 0)
    {
        std::cerr << "listen error" << std::endl;
        return 4;
    }
    //signal(SIGCHLD,SIG_IGN);//父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源
    for (;;)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len); // accept返回值是一个文件描述符
        if (new_sock < 0)
            continue;

        uint16_t cli_port = ntohs(peer.sin_port);
        std::string cli_ip = inet_ntoa(peer.sin_addr);

        std::cout << "get a new link-> : [" << cli_ip << ":" << cli_port <<"]# "<< new_sock << std::endl;

        pid_t id = fork();
        if(id < 0)
        {
            continue;
        }
        else if(id == 0)//曾经被父进程打开的fd,会被子进程继承,无论父子进程的哪一个,需要关闭文件描述符
        {
            //child
            close(listen_sock);
            if(fork() > 0) exit(0);
            ServiceIO(new_sock);
            close(new_sock);
            exit(0);
        }
        else
        {
            //parent
        }
    }
    return 0;
}

客户端代码:

#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <strings.h>
std::string Usage(std::string proc)
{
    std::cout << "Usage: 
	" << proc << "server_ip server_port" << std::endl;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string svr_ip = argv[1];
    uint16_t svr_port = atoi(argv[2]);
    // 1.创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0); // SOCK_STREAM---tcp
    if (sock < 0)
    {
        std::cerr << "socket error " << errno << std::endl;
        return 2;
    }

    //2.bind
    struct sockaddr_in server;
    bzero(&server,sizeof(server));
    server.sin_family = AF_INET;
    //将点分十进制的字符串风格deip转化为4字节ip
    //将4字节由主机序列转化为网络序列
    server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
    server.sin_port = htons(svr_port);
    //发起连接
    if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
    {
        std::cout << "connect server failed!" <<std::endl;
        return 3;
    }
    std::cout << "connect success!" << std::endl;

    //进行正常的业务请求
    while(true)
    {
        std::cout << "Please Enter# " ;
        char buffer[1024];
        fgets(buffer,sizeof(buffer)-1, stdin);

        write(sock, buffer, strlen(buffer));

        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }

    }
}

 

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。