Linux 网络编程API(一):基础

旧街凉风 提交于 2020-02-28 09:55:40

Linux网络API主要分为三大类:

socket地址API
socket基础API
网络信息API

socket地址API

大端字节序指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处,
反之小端字节序指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处

PC大多采用小端字节序,而网络字节序为大端字节序,解决方法为:发送端要把发送的数据转化成大端字节序数据后发送,而接收端知道对方传送过来的数据是大端字节序,再将其转化为小端字节序使用

Linux提供了以下四种API来进行主机字节序和网络字节序的转换

#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 long int netshort);

htonl 表示“host to network long”,即将长整型主机字节序转化为网络字节序,反之亦然
长整型通常用来转换IP地址,短整型通常用来转换端口号

socket地址
socket地址通常用一个结构体表示,里面包含了协议地址族,IP和端口号
地址族主要分为三类:
AF_UNIX : 本地域套接字
AF_INET : TCP/IPv4 协议族
AF_INET6 : TCP/IPv6 协议族

UNIX本地域套接字使用如下结构体

#include <sys/un.h>
struct sockaddr_un{
	sa_famiky_t sin_family; // 地址族:AF_UNIX
	char sun_path[108]; // 文件路径名
};

TCP/IP协议族有sockaddr_in和sockaddr_in6两个地址结构体表示,分别为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地址,要用网络字节序表示
};
// IPv6
struct sockaddr_in6{
	sa_family_t sin6_family; // 地址族:AF_INET6
	u_int16_t sin6_port; // 端口号,用网络字节序表示
	uint32_t sin6_flowinfo; // 流信息,设置为0
	struct in6_addr sin6_addr; // IPv6地址结构体,如下
	u_int32_t sin6_scope_id; // scope ID 
};
struct in6_addr{
	unsigned char sa_addr[16]; // IPv6地址,要用网络字节序表示
};

所有这些socket地址使用时都需转换为通用socket地址类型sockaddr,所有socket编程接口使用的地址类型也为sockaddr
用法:

struct sockaddr_in server_address;
...
bind(sockfd, (struct sockaddr*)&server_address,sizeof(server_address));

IP地址转换函数
人们习惯用可读性较好的字符串来表示IP地址,比如用点分十进制字符串表示IPv4地址,用十六进制字符串表示IPv6地址,编程时需要先将其转换为可读的整数才能使用.
如下

#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 函数将用点分十进制字符串表示的IPv4地址转换为用网络字节序表示的IPv4地址
inet_aton 函数完成和inet_addr通用的功能,但是将转化结果存储与参数inp指向的地址结构中,成功时返回1,失败时返回0
inet_ntoa 函数将用网络字节序整数表示的IPv4地址转化为点分十进制字符串表示的地址
*inet_ntoa 函数内部用一个静态变量存储转化结构,因此inet_ntoa是不可重入的.举例:

char* szValue1 = inet_ntoa("1.2.3.4");
char* szValue2 = inet_ntoa("10.194.71.60");
std::cout << "address 1: " << szValue1 << std::endl;
std::cout << "address 2: " << szValue2 << std::endl;

结果

address1: 10.194.71.60
address2: 10.194.71.60

下面三个函数也完成前面函数的功能,同样适用IPv4地址和IPv6地址

#include <arpa/inet.h>
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 函数将src表示的点分十进制字符串转换为网络字节序整数表示的IP地址并存于dst所指的内存中
inet_ntop 函数进行相反的转换,前三个参数与inet_pton的参数相同, 最后一个参数cnt指定目标存储单元的大小,有两个宏可以帮助我们进行指定

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

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

socket基础API

1. 创建socket ------socket()
2. 监听socket-------listen()
3. 接受连接----------accept()
4. 客户端发起连接-connect()
5. 关闭连接----------close(),shutdown()
6. TCP数据读写

创建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:该参数指定服务类型:服务类型主要有SOCK_STREAN(流服务)和SOCK_UGRAM(数据报)服务.对于TCP/IP协议族而言,取SOCK_STREAM表示传输层使用TCP协议, 取SOCK_DGRAM表示传输层使用UDP协议

protocol: 该参数是在前两个参数构成的协议集合下,选择一个具体的协议.不过这个值通常唯一,几乎所有情况下,都应该把它设置为0, 表示使用默认协议.
socket 系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno
用法:

int sockfd;
if(sockfd = socket(AF_INET,SOCK_STREAM,0) < 0){
	perror("socket error");
	exit(1);
}

监听socket
socket被命名之后,还不能马上接受客户连接,我们还需使用以下系统调用来创建一个监听队列存放待出口i的客户连接

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

sockfd: 为被监听的socket
backlog: 为内核监听队列的最大长度
监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息
未达到监听队列上限时,连接成功后状态为ESTABLISHED(完全连接状态),超过上限时客户端再进行连接,状态就变为SYN_RCVD状态(半连接状态)

listen成功时返回0, 失败则返回-1并设置errno
backlog 典型参数为5,但在Ubuntu18,04实际测试中,可以建立起6个连接,不同系统上运行结构会有差别,不过监听队列中完整连接上限通常大于backlog

接受连接
accept系统调用从listen监听队列中接受一个连接

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

实例

....
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr* )&client, &client_addrlength);
// client 和 client_addrlength 为传出参数

客户端发起连接
服务器通过listen调用来被动接受连接,客户端需要通过系统调用来主动与服务器建立连接

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

sockfd : 参数由socket系统调用返回一个socket.
serv_addr : 服务器监听的socket地址
addrlen: 这个地址的长度

connect成功返回0,一旦成功建立连接,sockfd就唯一标识了这个连接,客户端可以通过读写sockfd来与服务器通信.
connect失败则返回-1,并设置errno
两种常见的errno
ECONNREFUSED:目标端口不存在,连接被拒绝
ETIMEOUT:连接超时

关闭连接

  1. close
    关闭对应的socket即可,可以使用关闭普通文件描述符的系统调用
#include <unistd.h>
int close( int fd );

fd参数是待关闭的socket,不过close系统调用并非立即关闭一个连接,而是将fd的引用计数减1. 只有当fd的引用计数为0时,才能关闭连接.多进程程序中,每次fork都会默认使父进程中打开的socket的引用计数加1,因此必须在父进程和子进程中都对该socket进行close才能将连接关闭

  1. shutdown
    无论如何都要立即终止连接而不是将socket的引用计数减1, 可以使用shutdown系统调用
#include <sys/socket.h>
int shutdown(int sockfd, int howto );

sockfd: 待关闭的socket
howto: 该参数决定了shutdown的行为
SHUT_RD: 关闭sockfd上读的那一半,应用程序不能再针对socket文件描述符执行读操作,而且socket接收缓冲区中的数据都被丢弃
SHUT_WR: 关闭sockfd上写的这一半.sockfd的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该socket文件描述符执行写操作.这种情况下,连接处于半关闭状态
SHUT_RDWR: 同时关闭socket上的读和写, 或者都关闭. 而close在关闭连接时只能将socket上的读和写同时关闭.
shutdown成功返回0,失败返回-1并设置errno

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 flag );
ssize_t send(int sockfd, const void *buf, size_t len, int flags );

recv 读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,flag参数通常设为0即可.recv成功时返回读取到的数据的长度,它可能小于我们期望的长度len,recv返回0,意味着通信对方已经关闭连接了.recv出错时返回-1并设置errno

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

flags值为MSG_OOB表示发送或接受紧急数据

flags参数只对send和recv的当前调用生效,后面我们将看到如何通过setsockopt系统调用永久性地修改socket的某些属性

~ 待填坑

-------<LINUX 高性能服务器编程> —游双


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!