Linux高性能服务器编程|阅读笔记:第5章 - Linux网络编程基础API

举报
海轰Pro 发表于 2023/05/19 23:28:38 2023/05/19
【摘要】 简介Hello!非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出~ ଘ(੭ˊᵕˋ)੭昵称:海轰标签:程序猿|C++选手|学生简介:因C语言结识编程,随后转入计算机专业,获得过国家奖学金,有幸在竞赛中拿过一些国奖、省奖…已保研学习经验:扎实基础 + 多做笔记 + 多敲代码 + 多思考 + 学好英语! 唯有努力💪 本文仅记录自己感兴趣的内容 5.1 socket地址API 5.1....

简介

Hello!
非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出~
 
ଘ(੭ˊᵕˋ)੭
昵称:海轰
标签:程序猿|C++选手|学生
简介:因C语言结识编程,随后转入计算机专业,获得过国家奖学金,有幸在竞赛中拿过一些国奖、省奖…已保研
学习经验:扎实基础 + 多做笔记 + 多敲代码 + 多思考 + 学好英语!
 
唯有努力💪
 
本文仅记录自己感兴趣的内容

5.1 socket地址API

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

字节序

  • 大端字节序
  • 小端字节序

现在PC大多都使用小端字节序,所以小端字节序也被称为主机字节序
网络通信时,大多使用大端字节序,所以大端字节序也被称为网络字节序

#include <stdio.h>
using namespace std;
void byteorder()
{
	union
	{
		short value;
		char union_bytes[ sizeof( short ) ];
	} test;
	test.value = 0x0102;
	if (  ( test.union_bytes[ 0 ] == 1 ) && ( test.union_bytes[ 1 ] == 2 ) )
	{
		printf( "big endian\n" );
	}
	else if ( ( test.union_bytes[ 0 ] == 2 ) && ( test.union_bytes[ 1 ] == 1 ) )
	{
		printf( "little endian\n" );
	}
	else
	{
		printf( "unknown...\n" );
	}
}
int main() {
    byteorder();
    return 0;
}

一般两台机器通信时,都转为大端字节序进行通信

原因:若不转换,两个采用不同序列的主机就会发生数据读取错误解释

即使是同一台机器的两个进程,也要考虑字节序的问题

比如一个程序是c语音写的,另一个是java写的(java虚拟机采用大端字节序)

Linux中完成主机字节序和网络字节序之间的转换函数:

说明:

  • htonl:表示host to network long,意思是将long型的主机(host)字节序转为网络(network)字节序
  • s表示short
  • 一般long用来转换IP地址,short用来转换端口号

5.1.2 通用socket地址

表示socket地址的是一个结构体sockaddr

#include <sys/socket.h>
struct sockaddr
{
    unsigned short sa_family; /* address family, AF_xxx */
    char sa_data[14];         /* 14 bytes of protocol address */
};

sa_family:地址族

sa_data:用于存放socket地址值

5.1.3 专用socket地址

UNIX本地域协议族使用如下socket地址结构体

TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,分别用于IPv4和IPv6

注意:所有专用socket地址类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(使用强制转换即可)

5.1.4 IP地址转换函数

背景:对于IP地址,我们使用点分十进制,比如127.0.0.1,但是使用的时候我们需要将其转换为整数方便使用(IPv6也是如此)

点分十进制字符串表示的IPv4地址与网络字节序整数表示的IPv4地址之间的转换可以使用的3个函数

  • inet_addr:点分十进制字符串表示的IPv4地址 -> 网络字节序整数表示的IPv4地址,失败返回INADDR_NONE
  • inet_aton:效果与inet_addr一样,但是转换的结果是在参数inp指向的结构体中。函数返回值表示成功或失败,1表示成功,0表示失败
  • inet_ntoa:网络字节序整数表示的IPv4地址 -> 点分十进制字符串表示的IPv4地址。注意这个函数返回值是使用了一个静态变量存储转换结果,不可重入

每次调用都是写入同一块静态内存

同时使用IPv4和IPv6地址转换的函数

5.2 创建socket

socket本质就是一个可读、可写、可控制、可关闭的文件描述符

socket创建

domain:底层协议族
type:服务类型,SOCK_STREEAM(流服务)或SOCK_UGRAM(数据报)服务

5.3 命名socket

socket命名:将一个socket与socket地址绑定,也就是bind()

  • 服务端通常需要命名socket,这样客户端才知道如何去连接服务端
  • 客户端一般不需要命名socket,采用匿名方式,即使用操作系统自动分配的socket地址

命名socket的系统调用:bind()

5.4 监听socket

socket被命名之后,还不能马上接受客户连接

我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接

sockfd表示被监听的socket
backlog表示内核监听队列的最大长度

内核版本2.2之后,backlog只表示处于完全连接状态的socket上限


编写一个服务器程序,用以研究backlog参数对listen系统调用的实际影响

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>

static bool stop = false;
static void handle_term( int sig )
{
    stop = true;
}

int main( int argc, char* argv[] )
{
    signal( SIGTERM, handle_term );

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

    int sock = socket( PF_INET, SOCK_STREAM, 0 );
    assert( sock >= 0 );

    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 );

    int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );

    ret = listen( sock, backlog );
    assert( ret != -1 );

    while ( ! stop )
    {
        sleep( 1 );
    }

    close( sock );
    return 0;
}

5.5 接受连接

作用:从listen监听队列中接受一个连接,accept()


小测试:测试如果监听队列中处于established状态对连接对应的客户端出现网络异常或提前退出,那么服务端使用accept是否能成功?

#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>

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] );

    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 );

    int sock = socket( PF_INET, SOCK_STREAM, 0 );
    assert( sock >= 0 );

    int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );

    ret = listen( sock, 5 );
    assert( ret != -1 );

    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof( client );
    int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
    if ( connfd < 0 )
    {
        printf( "errno is: %d\n", errno );
    }
    else
    {
        char remote[INET_ADDRSTRLEN ];
        printf( "connected with ip: %s and port: %d\n", 
            inet_ntop( AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN ), ntohs( client.sin_port ) );
        close( connfd );
    }

    close( sock );
    return 0;
}

实验结果:accept只是从监听队列中取出连接,而不论连接处于何种状态(如上面的ESTABLISHED状态和CLOSE_WAIT状态),更不关心任何网络状况的变化。

5.6 发起连接

客户端通过connect系统调用主动与服务端建立连接

5.7 关闭连接

关闭此次连接对应的socket

close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减去1,只有当fd的引用计数为0时,才真正关闭连接。

在多进程程序中,一次fork系统调用将使父进程中打开的socket的引用计数 + 1
所以我们必须在父进程与子进程都对这个socket调用close时才将此连接关闭

如果是无论如何都要立即终止连接,那么则使用shutdown系统调用

5.8 数据读写

5.8.1 TCP数据读写

文件的读写操作read和write同样使用于socket

但socket也提供了几个专门用于socket数据读写的系统调用

其中用于TCP流数据读写的系统调用是:


小测试:发送带外数据

#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] );

    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 );

    int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( sockfd >= 0 );
    if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 )
    {
        printf( "connection failed\n" );
    }
    else
    {
        printf( "send oob data out\n" );
        const char* oob_data = "abc";
        const char* normal_data = "123";
        send( sockfd, normal_data, strlen( normal_data ), 0 );
        send( sockfd, oob_data, strlen( oob_data ), MSG_OOB );
        send( sockfd, normal_data, strlen( normal_data ), 0 );
    }

    close( sockfd );
    return 0;
}

小测试:接收带外数据

#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] );

    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 );

    int sock = socket( PF_INET, SOCK_STREAM, 0 );
    assert( sock >= 0 );

    int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );

    ret = listen( sock, 5 );
    assert( ret != -1 );

    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof( client );
    int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
    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 );
        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;
}

5.8.2 UDP数据读写

用于UDP数据报读写的系统调用是:

因为UDP通信没有连接的概念,所以每次读取数据都需要获取发送端的socket地址

5.8.3 通用数据读写函数

socket编程接口还提供了一对通用的数据读写系统调用。它们不仅能用于TCP流数据,也能用于UDP数据报:

5.9 带外标记

在实际应用中,我们通常无法预期带外数据何时到来

在实际应用中,我们通常无法预期带外数据何时到来

好在Liux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。

内核通知应用程序带外数据到达的两种常见方式是:

  • I/O复用产生的异常事件
  • SIGURG信号

但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这一点可通过如下系统调用实现:

sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。 如果是,sockatmark返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0。

带外数据(out—of—band data),有时也称为加速数据(expedited data),
是指连接双方中的一方发生重要事情,想要迅速地通知对方。
这种通知在已经排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。
带外数据设计为比普通数据有更高的优先级。
带外数据是映射到现有的连接中的,而不是在客户机和服务器间再用一个连接。

5.10 地址信息函数

作用:获取一个连接socket的本端socket地址以及远端的socket地址

getsockname:获取sockfd对应的本端socket地址

getpeername:获取sockfd对应的远端socket地址

5.11 socket选项

读取和设置socket文件描述符属性的方法

5.12 网络信息API

通过调用网络信息API实现主机名到IP地址的转换,以及服务名称到端口号的转换

比如下面两句命令效果是一样的

5.12.1 gethostbyname和gethostbyaddr

gethostbyname函数根据主机名称获取主机的完整信息

gethostbyaddr函数根据IP 地址获取主机的完整信息

5.12.2 getservbyname和getservbyport

getservbyname函数根据名称获取某个服务的完整信息

getservbyport函数根据端口号 获取某个服务的完整信息

5.12.3 getaddrinfo

getaddrinfo函数既能通过主机名获得IP地址(内部使用的是gethostbyname函数), 也能通过服务名获得端口号(内部使用的是getservbyname函数)

5.12.4 getnameinfo

getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)

结语

文章仅作为个人学习笔记记录,记录从0到1的一个过程

希望对您有一点点帮助,如有错误欢迎小伙伴指正

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。