Linux网络编程基础API

Posted _Karry

tags:

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

文章目录

概述

本文将从三个方面讨论Linux网络API:

  1. socket地址API。socket最开始含义是一个IP地址和端口对(ip, port)。它唯一确定了TCP通信的一端,称为socket地址
  2. socket基础API。socket的主要API都定义在sys/socket.h头文件中,包括创建socket、命名socket、监听socket、接受连接、发起连接、读写数据、获取地址信息、检测带外标记,以及读取和设置socket选项
  3. 网络信息API。Linux提供了一套网络信息API,以实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。这些API都定义在netdb.h头文件中

socket地址API

主机字节序和网络字节序

大端字节序是指一个整数的高位字节存储在内存的低位地址,低位字节存储在内存的高位地址。大端字节序也成为网络字节序。

小端字节序是指整数的高位字节序存储在内存的高位地址,低位字节序存储在内存的低位地址。现代PC大多采用小端字节序,因此小段字节序又称为主机字节序。

Linux提供了4个函数来完成主机字节序和网络字节序的转换:

#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

htonl表示"host to network long",即将长整型的主机字节序转换为网络字节序。
这4个函数中,长整型函数常用来转换IP地址,短整型函数常用来转换端口地址。

通用socket地址

socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:

#include <bits/socket.h>
struct sockaddr
	sa_family_t sa_family;	/* address family, AF_xxx */
	char sa_data[14];		/* 14 bytes of protocol address */

sa_family_t是地址族类型,地址族类型通常和协议族类型对应。
常见的协议族(protocol family,也称domain)和对应的地址族如表所示:

sa_data成员用于存放socket地址值。但是,不同的协议族的地址值具有不同的含义和长度,如表5-2所示:

由表5-2所示,14字节的sa_data根本无法完全容纳多数协议族的地址值。因此Linux定义了下面这个新的通用socket地址结构体:

#include <bits/socket.h>
struct sockaddr_storage

	sa_family_t sa_family;
	unsigned long int __ss_align;
	char __ss_padding[128-sizeof(__ss_align)];

这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)

专用socket地址

上面两个通用socket不太好用,所以Linux为各个协议族提供了专门的socket地址结构体。
UNIX本地域协议族使用如下专用socket地址结构体:

#include <sys/un.h>
struct sockaddr_un

	sa_family_t sin_family;  /* 地址族:AF_UNIX */
	char sun_path[108];		/* 文件路径名 */
;

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

struct sockaddr_in

	sa_family_t sin_family;	/*地址族:AF_INET*/
	u_int16_t sin_port;		/*端口号,要用网络字节序表示*/
	struct in_addr sin_addr;/*IPv4地址结构体,见下面*/
;
struct in_addr
	u_int32_t s_addr;		/*IPv4地址,要用网络字节序表示*/
;

struct sockaddr_in6
	sa_family_t sin6_family;	/*地址族:AF_INET6*/
	u_int16_t sin6_port;		/*端口号,要用网络字节序表示*/
	u_int32_t sin6_flowinfo;	/*流信息,应设置为0*/
	struct in6_addr sin6_addr;	/*IPv4地址结构体,见下面*/
	u_int32_t sin6_scope_id;	/*socpe ID,尚处于实验阶段*/
;
struct in6_addr

	unsigned char sa_addr[16];	/*IPv6地址,要用网络字节序表示*/
;

所有专用socket地址类型的变量在实际使用时都需要转换为通用socket地址类型sockaddr(强制转换即可),因为所有的socket编程接口使用的地址参数类型都是sockaddr。

IP地址转换函数

通常人们用点分十进制来表示IPv4地址,用十六进制字符串表示IPv6地址。但编程中我们需要转换为整数(二进制数)才能使用。
而记录日志时,我们需要把整数表示的IP地址转换为点分十进制
下面三个函数可以用于点分十进制表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:

#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp, struct in_addr* inp);
char* inet_ntoa(struct in_addr in);

inet_addr函数将点分十进制地址转换为网络字节序整数表示的地址,失败返回INADDR_NONE。
inet_aton函数和inet_addr函数功能相同,但将转换结果存储于参数inp指向的地址结构中。成功返回1,失败返回0。
inet_ntoa函数将网络字节序表示的地址转换为点分十进制地址。

下面这对更新的函数也能完成上面三个函数的功能,同时也适用于IPv4和IPv6

#include <arpa/inet>
int inet_pton(int af, const char* src, void* dst);
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);

inet_pton函数将字符串表示的IP地址src转换为网络字节序整数表示的IP地址,并存储在dst指向的内存中;af参数指定地址族,可以是AF_INET或者AF_INET6。成功返回1,失败返回0,并设置errno

inet_ntop函数进行相反的转换,前三个参数具有相同的含义,最后一个cnt指定目标存储单元的大小,下面两个宏用于指定大小

#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

inet_ntop成功返回目标存储单元的地址,失败返回NULL并设置errno。

创建socket(socket函数)

在Linux里,socket是一个可读、可写、可控制、可关闭的文件描述符。下面创建一个socket:

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

domain参数告诉系统使用哪个底层协议族。对TCP/IP协议,参数设置为AF_INET(IPv4)或AF_INET6(IPv6);对于UNIX本地域协议族,参数设置为AF_UNIX

type参数指定服务类型。对TCP/IP而言,取SOCKET_STREAM;对UDP协议,取SOCKET_DGRAM。

protocol一般设置为0,表示使用默认协议。

socket系统调用成功返回一个socket文件描述符,失败返回-1并设置errno。

命名socket(bind函数)

创建socket时,我们指定了地址族,但并未指定使用地址族中哪个具体的socket地址。
将一个socket与socket地址绑定称为给socket命名。
在服务器程序中,我们通常要命名socket,因为命名后客户端才知道如何连接它;客户端通常不需要命名socket,采用操作系统自动分配的socket地址。
命名socket的系统调用是bind,其定义如下:

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);

bind 将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址的长度,即sizeof(my_addr)。

bind成功返回0,失败返回-1并设置errno

例子:

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);

首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666

监听socket(listen函数)

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

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

sockfd参数指定被监听的socket
backlog指定内核监听队列的最大长度,监听队列长度如果超过backlog,服务器将不受理新的客户连接,客户端也收到ECONNREFUSED的错误。

listen成功返回0,失败返回-1并设置errno

接受连接(accept函数)

accpet函数从listen监听队列中接受一个连接:

#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockdf: 文件描述符
addr: 传出参数,返回连接客户端地址信息,含IP地址和端口号
addrlen: 传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小

返回值:成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno

三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
addr是一个传出参数,accept()返回时传出客户端的地址和端口号。
addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址

使用举例:

while (1) 
	cliaddr_len = sizeof(cliaddr);
	connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
	n = read(connfd, buf, MAXLINE);
	......
	close(connfd);

整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1

发起连接(connect函数)

服务器通过listen调用来被动接受连接;客户端通过connect函数调用来主动与服务器建立连接:

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

sockfd: 文件描述符
serv_addr: 传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen: 传入参数,传入sizeof(addr)大小

返回值: 成功返回0,失败返回-1,设置errno

客户端需要调用connect()连接服务器,connectbind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。

关闭连接(close函数)

关闭一个连接实际上就是关闭该连接对应的socket

#include <unistd.h>
int close(int fd);

fd是待关闭的socket。

不过close系统调用并非立即关闭一个连接,而是将fd的引用计数-1,当fd的计数为0时,才真正关闭连接。
在多进程程序中,一次fork系统调用默认使父进程中打开的socket的引用计数+1,因此我们必须在父进程和子进程中都对该socket执行close才能将连接关闭。

如果要立即终止连接,可以使用shutdown系统调用

#include <sys/socket.h>
int shutdown(int sockfd, int howto);

sockfd是待关闭的socket
howto参数决定shutdown行为,可取值如下表

数据读写

TCP数据读写

对文件的读写read和write同样适用于socket。但是socket提供了几个专门用于socket数据读写的系统调用。用于TCP流数据读写的系统调用是:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

recv 读取sockfd上的数据,buf和len分别指读缓冲区的位置和大小,flags常设为0。
recv成功时返回实际读取到的数据长度,出错返回-1并设置errno。

send往sockfd上写入数据,buf和len分别指定写缓冲区的位置和大小。
send成功时返回实际写入的数据的长度,失败返回-1并设置errno。

flags参数为数据收发提供了额外的控制,它的取值如表:(一般取0)

例子:
发送带外数据(服务器端)

#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>

int main()

	// 1.创建监听套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1)
        perror("socket");
        return -1;
    
	
	// 2.将服务器端的IP地址和端口号绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);	//从本地字节序转换为网络字节序
    // INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
    // 这个宏可以代表任意一个IP地址
    // 这个宏一般用于本地的绑定操作
    saddr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
    if(ret == -1)
        perror("bind");
        return -1;
    

	// 3.设置监听
    ret = listen(fd, 100);
    if(ret == -1)
        perror("listen");
        return -1;
    
    
    // 4.阻塞等待并接受客户端的连接
    struct sockaddr_in caddr;
    int caddrlen = sizeof(caddr);
    int cfd = accept(fd, (struct sockaddr*)&caddr, &caddrlen);
    if(cfd == -1)
    
        perror("accept");
        return -1;
    
	// 输出客户端的IP和端口号
    char ip[32];
    printf("client IP: %s, port: %d\\n",
           inet_ntop(AF_INET, &caddr.sin_addr.s_addr, ip, sizeof(ip)),
           ntohs(caddr.sin_port));

	// 5.和客户端通信
    while(1)
    
    	// 接收数据
        char buf[1024];
        int len = recv(cfd, buf, sizeof(buf), 0);
        if(len > 0)
            printf("client says: %s\\n", buf);
            // 发送数据
            send(cfd, buf, len, 0);
        
        else if(len == 0)
            printf("client break connection\\n");
            break;
        
        else
            perror("read");
            break;
        
    
    
    close(fd);
    close(cfd);

    return 0;

接收带外数据:

#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>

int main()

	// 1.创建通信的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1)
        perror("socket");
        return -1;
    

	// 2.连接服务器
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);	// 本地字节序转换为网络字节序
    inet_pton(AF_INET, "192.168.143.141", &saddr.sin_addr.s_addr);
    int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
    if(ret == -1)
        perror("connect");
        return -1;
    

	// 和服务器通信
    int number = 0;
    while(1)
    
    	// 发送数据
        char buf[1024];
        sprintf(buf, "Hello, world, %d...\\n", number++);
        send(fd, buf, (strlen(buf)+1), 0);

		// 接收数据
        memset(buf, 0, sizeof(buf));
        int len = recv(fd, buf, sizeof(buf), 0);
        if(len > 0)
        
            printf("server says: %s\\n", buf);
        
        else if(len == 0)
        
            printf("server break connection\\n");
            break;
        
        else
        
            perror("recv");
            break;
        
        sleep(2);
    

    close(fd);

    return 0;

以上是关于Linux网络编程基础API的主要内容,如果未能解决你的问题,请参考以下文章

linux学习 --- 网络基础知识

C语言中数组高位转为低位

C语言 对字节的高位和低位进行互换!

网络通信中字节序的理解

Java 高位低位

Linux网络编程---htons函数的使用