Linux C++下网络编程之基础API

发布于:2022-11-29 ⋅ 阅读:(351) ⋅ 点赞:(0)

1. C/S模型

在正式讲解socket编程API前,先来简单回顾一下C/S(客户端/服务器)模型。C/S模型很简单,一言以蔽之就是,所有客户端都通过访问服务器来获取所需要的资源,用图表示如下:
C/S模型

采用C/S模型的TCP服务器和TCP客户端的工作流程如下图所示:
TCP服务器和TCP客户端的工作流程

对照上图,概括一下C/S模型的逻辑。服务器启动后,首先通过socket()函数创建一个socket,并调用bind()函数将其与具体的服务器地址和端口绑定(绑定后这个socket就和服务器上的某个端口关联上了,以后操作这个socket就相当于在和运行在服务器端口上的某个应用程序交互);然后调用listen()函数监听这个socket,等待客户连接(服务器处于被动连接状态);待服务器稳定运行后,客户端通过connect()函数向服务器发起连接(客户端主动连接);由于客户连接请求是随机到达的异步事件,服务器需要使用某种I/O模型来监听这一事件,I/O模型有多种,图中使用的是select系统调用;当监听到连接请求后,服务器就调用accept()函数接受它,并分配一个逻辑单元为新的连接服务,逻辑单元可以是新创建的子进程、子线程或者其他,图中服务器给客户端分配的逻辑单元是由fork系统调用创建的子进程;下面就是服务器与客户端之间的数据交互了,一方发送、另一方接收;最后,客户端通过close()函数主动关闭连接,服务器接收到关闭请求后,执行被动关闭连接。

2. 基础API

下面挨个介绍一下各个API的原型,这里我的机器系统内核版本是Linux 5.4.0-128-generic,不同内核的实现方式可能不同。

2.1 socket地址API

2.1.1 主机字节序和网络字节序

要学习socket地址API,先要理解主机字节序网络字节序

  • 主机字节序(小端字节序):整数的高位字节存储在内存高地址处,低位字节存储在内存低地址处
  • 网络字节序(大端字节序):整数的高位字节存储在内存低地址处,低位字节存储在内存高地址处

2.1.2 通用socket地址

#include <bits/socket.h>		// 所在头文件

/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };
#include <bits/sockaddr.h>

/* This macro is used to declare the initial common members
   of the data types used for socket addresses, `struct sockaddr',
   `struct sockaddr_in', `struct sockaddr_un', etc.  */

#define	__SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

表示通用socket地址的结构体是socketaddr,它的两个数据成员为:

  • sa_family: 地址族类型(sa_family_t)变量
  • sa_data: 存放socket地址值(ip和port)

2.1.3 专用socket地址

#include <netinet/in.h>

/* Structure describing an Internet socket address.  */
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用的socket地址结构体,分别用于IPv4和IPv6,上面列出的是常用的sockaddr_in结构,它的主要成员是开始的三个,正常使用时也就设置前三个即可:

  • sin_family: 地址族 AF_INET
  • sin_port: 端口号,要用网络字节序表示
  • sin_addr: IPv4地址结构体,见下
#include <netinet/in.h>

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

in_addr结构体中记录了IPv4地址,要用网络字节序表示

2.1.4 IP地址转换函数

在实际应用中,我们习惯用点分十进制字符串来表示IPv4地址,但在编程中我们需要把它们转化成整数(二进制数)方能使用。linux提供了以下3个函数进行点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:

#include<arpa/inet.h>

/* Convert Internet host address from numbers-and-dots notation in CP
   into binary data in network byte order.  */
extern in_addr_t inet_addr (const char *__cp) __THROW;

/* Convert Internet host address from numbers-and-dots notation in CP
   into binary data and store the result in the structure INP.  */
extern int inet_aton (const char *__cp, struct in_addr *__inp) __THROW;

/* Convert Internet number in IN to ASCII representation.  The return value
   is a pointer to an internal array containing the string.  */
extern char *inet_ntoa (struct in_addr __in) __THROW;

说明:

  • inet_addr 函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址
  • inet_aton 函数完成和 inet_addr 函数一样的功能,但是将转化结果存储于参数 __inp 指向的地址结构中
  • inet_ntoa 函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址

下面这对更新的函数能完成和上述3个函数同样的功能,并且同时适用于IPv4和IPv6地址,所以我们在网络编程中更多的是使用这两个函数:

#include<arpa/inet.h>

/* Convert from presentation format of an Internet number in buffer
   starting at CP to the binary network format and store result for
   interface type AF in buffer starting at BUF.  */
extern int inet_pton (int __af, const char *__restrict __cp,
		      void *__restrict __buf) __THROW;

/* Convert a Internet address in binary network format for interface
   type AF in buffer starting at CP to presentation form and place
   result in buffer of length LEN astarting at BUF.  */
extern const char *inet_ntop (int __af, const void *__restrict __cp,
			      char *__restrict __buf, socklen_t __len)
     __THROW;

说明:

  • inet_pton 函数将用字符串表示的IP地址(__cp)转换成用网络字节序整数表示的IP地址,并把转换结果存储于 __buf 指向的内存中;__af 参数指定地址族,可以是 AF_INET 或 AF_INET6
  • inet_ntop 函数执行相反的转换,前三个参数含义与 inet_pton 的参数相同,最后一个参数 __len 指定目标存储单元的大小

2.2 创建socket

前面说到,我们可以通过socket()系统调用来创建一个socket,那什么是socket呢?在linux中,一切皆文件。socket是一个可读、可写、可控制、可关闭的文件描述符,它和磁盘文件描述符一样,只不过磁盘文件描述符关联的是磁盘上的文件,而socket关联的是服务器上的端口号。

#include <sys/socket.h>

/* Create a new socket of type TYPE in domain DOMAIN, using
   protocol PROTOCOL.  If PROTOCOL is zero, one is chosen automatically.
   Returns a file descriptor for the new socket, or -1 for errors.  */
extern int socket (int __domain, int __type, int __protocol) __THROW;

参数说明:

  • __domain:指明使用哪个协议族,对于TCP/IP协议族而言,应设置为 PF_INET(IPv4)或者 PF_INET6(IPv6)
  • __type:对于TCP/IP协议族而言,其值取 SOCK_STREAM 表示使用TCP协议,SOCK_UGRAM 表示使用UDP协议
  • __protocol:设置为0,使用默认协议

2.3 命名socket

命名socket,就是将一个socket与socket地址绑定。在服务器程序中,通常需要命名socket,因为只有命名后客户端才能知道该如何连接它;客户端则通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址。命名socket的系统调用是bind(),其定义如下:

#include <sys/socket.h>

/* Give the socket FD the local address ADDR (which is LEN bytes long).  */
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
     __THROW;

参数说明:

  • __fd:未命名的socket文件描述符
  • __addr:要绑定的socket地址
  • __len:socket地址长度

2.4 监听socket

服务器的socket被命名后,还不能马上接受客户端的连接,需要先使用listen()系统调用来创建一个监听队列以存放待处理的客户连接:

#include <sys/socket.h>

/* Prepare to accept connections on socket FD.
   N connection requests will be queued before further requests are refused.
   Returns 0 on success, -1 for errors.  */
extern int listen (int __fd, int __n) __THROW;

参数说明:

  • __fd:(执行过bind系统调用的)被监听的socket文件描述符
  • __n:监听队列的最大长度

2.5 接受连接

服务器通过accept()系统调用被动地从listen监听队列中接受一个连接:

#include <sys/socket.h>

/* Await a connection on socket FD.
   When a connection arrives, open a new socket to communicate with it,
   set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
   peer and *ADDR_LEN to the address's actual length, and return the
   new socket's descriptor, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int accept (int __fd, __SOCKADDR_ARG __addr,
		   socklen_t *__restrict __addr_len);

参数说明:

  • __fd:(执行过listen系统调用的)监听socket文件描述符
  • __addr:用来获取被接受连接的远端(客户端)socket地址
  • __addr_len:被接受连接的远端socket地址长度指针

2.6 发起连接

客户端通过connect()系统调用主动地与服务器建立连接:

#include <sys/socket.h>

/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
   For connectionless socket types, just set the default address to send to
   and the only address from which to accept transmissions.
   Return 0 on success, -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);

参数说明:

  • __fd:socket系统调用返回的socket文件描述符(匿名,无需bind)
  • __addr:服务器监听的socket地址
  • __addr_len:服务器监听socket地址的长度

2.7 关闭连接

关闭连接就是关闭该连接对应的socket,可通过close()系统调用来完成:

#include <unistd.h>

/* Close the file descriptor FD.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int close (int __fd);

参数说明:

  • __fd:待关闭的socket文件描述符

2.8 数据读写

2.8.1 TCP数据读写

TCP是面向连接的,所以读写数据时只需指定本端连接套接字:

#include <sys/socket.h>

/* Send N bytes of BUF to socket FD.  Returns the number sent or -1.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);

/* Read N bytes into BUF from socket FD.
   Returns the number read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);

说明:

  • send 函数往 __fd 上写入数据,__buf 和 __n 参数分别指定写缓冲区的位置和大小
  • recv 函数读取 __fd 上的数据,__buf 和 __n 参数分别指定读缓冲区的位置和大小,__flags 参数含义可查阅相关手册,通常设置为0即可

2.8.2 UDP数据读写

由于UDP通信没有连接的概念,所以每次读取/发送数据都需要指定发送端/接收端的socket地址:

#include <sys/socket.h>

/* Send N bytes of BUF on socket FD to peer at address ADDR (which is
   ADDR_LEN bytes long).  Returns the number sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendto (int __fd, const void *__buf, size_t __n,
		       int __flags, __CONST_SOCKADDR_ARG __addr,
		       socklen_t __addr_len);

/* Read N bytes into BUF through socket FD.
   If ADDR is not NULL, fill in *ADDR_LEN bytes of it with tha address of
   the sender, and store the actual size of the address in *ADDR_LEN.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n,
			 int __flags, __SOCKADDR_ARG __addr,
			 socklen_t *__restrict __addr_len);

说明:

  • sendto 函数往 __fd 上写入数据,__buf 和 __n 参数分别指定写缓冲区的位置和大小,__addr 参数指定接收端的socket地址,__addr_len 参数则指定该地址的长度
  • recvfrom 函数读取 __fd 上的数据,__buf 和 __n 参数分别指定读缓冲区的位置和大小,__addr 参数指定发送端的socket地址,__addr_len 参数则指定该地址的长度

3. 示例代码

  • server.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define BUF_SIZE 1024

int main(int argc, char* argv[])
{
    if (argc <= 2)
    {
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);
	
	// 创建并初始化服务端socket地址
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);
	
	// 1. 创建服务端socket	SOCK_STREAM:流服务,TCP协议
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);
	
	// 2. 将socket与socket地址绑定
    int ret = bind(sock, (const sockaddr*)&address, sizeof(address));
    assert(ret != -1);
	
	// 3. 监听socket(等待客户端的连接)
    ret = listen(sock, 5);
    assert(ret != -1);
	
	// 创建客户端socket地址(用于记录客户端的socket地址,其值在accept接受连接时确认)
    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
	
	// 4. 服务端被动连接
    int connfd = accept(sock, (struct sockaddr* )&client, &client_addrlength);	 	// connfd:连接socket,connfd是一个新生成的socket,与sock不是一回事
    if (connfd < 0)
    {
        printf("errno is: %d\n", errno);
    }
    else
    {
        char buffer[BUF_SIZE];

        memset(buffer, '\0', BUF_SIZE);
        ret = recv(connfd, buffer, BUF_SIZE - 1, 0);      // 从socket中读取数据,放置在buffer中
        printf("got %d bytes of normal data '%s'\n", ret, buffer);

        memset(buffer, '\0', BUF_SIZE);
        ret = recv(connfd, buffer, BUF_SIZE - 1, MSG_OOB);
        printf("got %d bytes of oob data '%s'\n", ret, buffer);

        memset(buffer, '\0', BUF_SIZE);
        ret = recv(connfd, buffer, BUF_SIZE - 1, 0);
        printf("got %d bytes of normal data '%s'\n", ret, buffer);

        close(connfd);
    }
    
    close(sock);
    
    return 0;
}
  • client
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
    if (argc <= 2)
    {
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

  	// 创建并初始化客户端socket地址
    struct sockaddr_in server_address;
    bzero(&server_address, sizeof(server_address));
    server_address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &server_address.sin_addr);
    server_address.sin_port = htons(port);

    // 创建客户端socket,客户端使用操作系统自动分配的socket地址,因此无需像服务端那样执行bind操作
    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(sockfd >= 0);

    // 客户端主动发起连接
    if (connect(sockfd, (const sockaddr*)&server_address, sizeof(server_address)) < 0)
    {
        printf("connection failed!\n");
    }
    else
    {
        const char* oob_data = "abc";
        const char* normal_data = "123";
        send(sockfd, normal_data, strlen(normal_data), 0);  // 将normal_data处的数据发向客户端socket
        send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
        send(sockfd, normal_data, strlen(normal_data), 0);
    }

    close(sockfd);
    
    return 0;
}

4. 参考书籍

Linux高性能服务器编程

本文含有隐藏内容,请 开通VIP 后查看