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

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高性能服务器编程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
这是一门linuxc++通讯架构实战课程,针对c/c++语言已经掌握的很熟并希望进一步深造以将来用c++linux下从事网络通讯领域/网络服务器的开发和架构工作。这门课程学习难度颇高但也有着极其优渥的薪水(最少30K月薪,最高可达60-80K月薪),这门课程,会先从nginx源码的分析和讲解开始,逐步开始书写属于自己的高性能服务器框架代码,完善个人代码库,这些,将会是您日后能取得高薪的重要筹码。本课程原计划带着大家逐行写代码,但因为代码实在过于复杂和精细,带着写代码可能会造成每节课至少要4~5小时的超长时间,所以老师会在课前先写好代码,主要的时间花费在逐行讲解这些代码上,这一点望同学们周知。如果你觉得非要老师领着写代码才行的话,老师会觉得你当前可能学习本门课程会比较吃力,请不要购买本课程,以免听不懂课程并给老师差评,差评也会非常影响老师课程的销售并造成其他同学的误解。 这门课程要求您具备下面的技能:(1)对c/c++语言掌握的非常熟练,语言本身已经不是继续学习的障碍,并不要求您一定熟悉网络或者linux;(2)对网络通讯架构领域有兴趣、勇于挑战这个高难度的开发领域并期望用大量的付出换取高薪;在这门课程中,实现了一个完整的项目,其中包括通讯框架和业务逻辑框架,浓缩总结起来包括如下几点:(1)项目本身是一个极完整的多线程高并发的服务器程序;(2)按照包头包体格式正确的接收客户端发送过来的数据包, 完美解决收包时的数据粘包问题;(3)根据收到的包的不同来执行不同的业务处理逻辑;(4)把业务处理产生的结果数据包正确返回给客户端;本项目用到的主要开发技术和特色包括:(1)epoll高并发通讯技术,用到的触发模式是epoll中的水平触发模式【LT】;(2)自己写了一套线程池来处理业务逻辑,调用适当的业务逻辑处理函数处理业务并返回给客户端处理结果;(3)线程之间的同步技术包括互斥量,信号量等等;(4)连接池中连接的延迟回收技术,这是整个项目中的精华技术,极大程度上消除诸多导致服务器程序工作不稳定的因素;(5)专门处理数据发送的一整套数据发送逻辑以及对应的发送线程;(6)其他次要技术,包括信号、日志打印、fork()子进程、守护进程等等;

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MinBadGuy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值