Linux socket API

socket是进程通信机制的一种,与PIPE、FIFO不同的是,socket即可以在同一台主机通信(unix domain),也可以通过网络在不同主机上的进程间通信(如:ipv4、ipv6),例如因特网,应用层通过调用socket API来与内核TCP/IP协议栈的通信,通过网络字节实现不用主机之间的数据传输。

前置条件

字节序

对于多字节的数据,不同处理器存储字节的顺序称为字节序,主要有大端序(big-endian)和小端序(little-endian),字节序的收发不统一就会导致值被解析错误。

大端序

高位字节存低位内存

大端序是最高位字节存储在最低位内存地址处。例如一段数据0x0A0B0C0D,0x0A是最高位字节,0x0D是最地位字节,内存地址最低位a、最高位a+3,在大端序中存储方式如下

  • 8bit存储方式:内存地址从低到高0x0A -> 0x0B -> 0x0C -> 0x0D
  • 16bit存储方式:内存地址从低到高0x0A0B -> 0x0C0D

小端序

低位字节存低位内存

小端序是最低位字节存储在最低位内存地址处。例如一段数据0x0A0B0C0D,0x0A是最高位字节,0x0D是最地位字节,内存地址最低位a、最高位a+3,在小端序存储方式如下

  • 8bit存储方式:内存地址从低到高0x0D->0X0C->0X0B->0X0A
  • 16bit存储方式:内存地址从低到高0X0C0D->0X0A0B

主机通常使用小端序,因为计算机先处理小端序的字节效率更高。通过上面的结构不难看出,大端序更易读,所以网络和存储等采用了大端序,那么网络通信的时候就需要将网络字节的大端序转换为主机字节的小端序。好在这些都有系统调用可以保证~
判断主机的字节序:

#include <iostream>
using namespace std;
void byteorder() {
  union {
    short value;
    char union_bytes[sizeof(short)];
  } test;
  test.value = 0x0102;
  if ((test.union_bytes[0] == 0x01) && (test.union_bytes[1] == 0x02)) {
    cout << "big endian" << endl;  // [0x01, 0x02]
  } else if ((test.union_bytes[0] == 0x02) && (test.union_bytes[1] == 0x01)) {
    cout << "little endian" << endl;  // [0x02, 0x01]
  } else {
    cout << "unknow~" << endl;
  }
}

int main() { byteorder(); }

字节序转换

#include<netinet/in.h>
// long型主机字节序转换为long型网络字节序, host to network
unsigned long int htonl(unsigned long int hostlong);
// short型
unsigned short int htons(unsigned short int hostshort);
// long型网络字节序转换为long型主机字节序, network to host
unsigned long int ntohl(unsigned long int netlong);
// short型
unsigned short int ntohs(unsigned short int netshort);

比方转换主机的端口

int main(int argc, char *argv[]){
    int port = atoi(argv[1]); // 主机序
    server_address.sin_port = htons(port); // 网络序
}

地址

通用地址

地址我们标识通信的端点,通用的地址格式为

#include<bits/socket.h>
struct sockaddr
{
    sa_family_t sa_family; // 协议类型,例如 ipv4 AF_INET、unix AF_UNIX
    char sa_data[14]; // unix域存放文件路径,ip域存放ip地址和端口号
}

sa_data只能容纳14字节地址数据,如果是unix域路径长度可以达到108字节放不下,所以linux定义了新的地址

#include<bits/socket.h>
struct sockaddr_storage
{
    sa_family_t sa_family;
    unsigned long int__ss_align; // 作用是内存对齐
    char__ss_padding[128-sizeof(__ss_align)];
}

专有地址

专有地址在bind、accept、connect等需要用到的函数中需要强制转换为通用地址,例如:(struct sockaddr *)&server_address

顾名思义专门为ipv4、unix、ipv6设计的不同socket地址结构,以ipv4为例

struct sockaddr_in
{
    sa_family_t sin_family; // AF_INET
    u_int16_t sin_port; // 网络字节序的端口号
    struct in_addr sin_addr; // IP地址
};
struct in_addr
{
    u_int32_t s_addr; // 网络字节序的IP地址
};

具体这样用:

int main(int argc, char *argv[]) {
    const char *ip = argv[1]; // 主机序ip地址
    int port = atoi(argv[2]); // 主机序端口
    struct sockaddr_in address; // ipv4专有地址
    // 设置专有地址的成员
    address.sin_family = AF_INET;
    address.sin_port = htons(port);
	// 将点分10进制的ip字符串转换为网络字节序整形表示的ip地址,存入sin_addr
    inet_pton(AF_INET, ip, &address.sin_addr);
  	int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建socket
    // 绑定端口,要强制转换为通用地址 (struct sockaddr *)&address
  	int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
}

创建连接

创建socket

Linux一切皆文件,所以socket创建好之后就是一个文件描述符,对该fd读写关闭、属性控制。

以ipv4为例

#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  • 第一个参数domain指定协议族,AF_INET、AF_UNIX、AF_INET6
  • 第二个参数type指定socket类型,TCP\UDP分别使用流式SOCK_STREAM和数据报式SOCK_DGRAM
  • 第三个参数protocal指定协议,有IPPROTO_TCP、IPPROTO_ICMP、IPPROTO_UDP等。通常使用默认的0。例如domain为AF_INET,type为SOCK_STREAM,那么就意味着ipv4 TCP类型的socket,protocal设置为0即可。

标识socket:bind

标识该socket,对于ipv4用ip地址和端口作为端点的表示

int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));

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

  • EACCES:没有权限绑定该端口
  • EADDRINUSE:绑定一个没有释放的端口和地址,通常被处于TIME_WAIT的连接使用,需要使用SO_REUSEADDR来复用处于TIME_WAIT连接的端口和地址

监听socket:listen

开始监听,并指定连接数

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

ret = listen(sock, 5);
  • backlog参数表示处于ESTABLISHED状态的连接数(我的ubuntu20.4测试为backlog+1),超过该值客户端收到ECONNREFUSED或者客户端TIMEOUT

接受连接:accept

从listen队列中拿连接过来,不管该连接是ESTABLISED还是CLOSE_WAIT的状态。

int connfd = accept(sockfd, (struct sockaddr *)&client, &client_addrlength);

发起连接:connect

connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address))

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

  • ECONNREFUSED:目标端口不存在或连接被拒绝
  • ETIMEOUT:连接超时

关闭连接

close

关闭socket fd,默认情况下:如果是多进程,fork后会将fd引用计数加1,如果要关闭该socket,父子进程都需要close,而且是同时关闭读和写。可以通过setsockopt的SO_LINGER控制close的行为

#include<sys/socket.h>
struct linger
{
	int l_onoff; // 关闭控制
	int l_linger; // 控制时间
}

close可能会有三种行为:

  1. l_onoff:关闭时(值为0),close默认行为,发送缓冲区所有数据后关闭连接
  2. l_onoff:打开时(值大于0),若l_linger为0,close系统调用立即返回,缓冲区数据被丢弃,给对端发送RST报文
  3. l_onoff:打开时(值大于0),若l_linger大于0:
    1. 阻塞型socket,close等待l_linger的时间,直到发送完缓冲区数据并收到对端的ACK,如果这段时间没有发送完缓冲区数据并收到确认,close将返回-1并设置errno为EWOULDBLOCK。
    2. 非阻塞型socket,立即返回,根据返回值和errno来判断残留数据是否发送完毕

shutdown

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

不引用计数直接关闭,howto参数:

  • SHUT_RD:程序不能再对socketfd做读操作,接收缓冲区数据被丢掉
  • SHUT_WR:关闭socketfd写,缓冲区数据会在关闭前发送出去,写操作不可执行(半关闭状态)
  • SHUT_RDWR:同时关闭

数据读写

除了默认对文件描述符的read、write操作之外,socket提供了专门的读写数据函数

TCP读写(recv & send)

#include<sys/socket.h>
// recv成功时返回读取到的长度,实际长度可能小于len
// 发生错误返回-1设置errno,返回0表示连接关闭
ssize_t recv(int sockfd, void*buf, size_t len, int flags);

// 成功时返回写入的数据的长度,失败返回-1这是errno
ssize_t send(int sockfd, const void*buf, size_t len, int flags);

flags提供了一些选项设置:

  • MSG_OOB(recv&send):发送或接收紧急数据,也叫带外数据,在传输层的七七八八中首部信息中有说,在URG标志位1时该字段有效,seq + Urgen Pointer - 1的这一个字节是紧急数据(紧急数据只有一个字节),例如:
char buffer[1024];
memset(buffer, '\0', 1024);

// 发送端发送带外数据hello
const char *oob_data = "hello";
send(sockfd, oob_data, strlen(oob_data), MSG_OOB);

ret = recv(connfd, buffer, BUFESIZE - 1, 0);
// 接收到hell
ret = recv(connfd, buffer, BUFESIZE - 1, MSG_OOB); // 接收端接收带外数据
// 接收到o

hell为正常数据,o为带外数据,只有最后一个字节会被认为是带外数据,前面的是正常数据。正常数据的接收会被带外数据截断。

  • int sockatmark(int sockfd);可以判断下一个数据是不是带外数据,1为是,此时可以利用MSG_OOB标志的recv调用来接收带外数据。
  • 通过SIGUSR信号触发对带外数据的处理
  • MSG_DONTWAIT(recv&send):对socket的此次send或recv是非阻塞操作(相当于使用O_NONBLOCK)
  • MSG_WAITALL(recv):一直读取到请求的数据全部返回后recv函数返回

UDP读写(recvfrom & sendto)

通常这两个函数用于无连接的套接字,如果用于有连接的读写可以把后两位置为NULL

#include <sys/socket.h>
// 可以接收UDP,也可以接收TCP(后两个参数置位NULL,因为TCP是面向连接的)
ssize_t recvfrom(int sockfd,void* buf,size_t len,int flags,
				struct sockaddr* src_addr,socklen_t* addrlen);
// 可以接收UDP,也可以接收TCP(后两个参数置位NULL,因为TCP是面向连接的)
ssize_t sendto(int sockfd,const void* buf,size_t len,int flags,
				const struct sockaddr* dest_addr,socklen_t addrlen);

更高级的读写(recvmsg & sendmsg)

使用sendmsg可以将多个缓冲区的数据合并发送
使用recvmsg可以将接收的数据送入多个缓冲区,或者接收辅助数据

#include<sys/socket.h>
ssize_t recvmsg(int sockfd,struct msghdr* msg,int flags);
ssize_t sendmsg(int sockfd,struct msghdr* msg,int flags);

msghdr结构

struct msghdr
{
    void* msg_name; // socket地址,如果是流数据,设置为NULL
    socklen_t msg_namelen; // 地址长度
    struct iovec* msg_iov; // I/O缓存区数组,分散的缓冲区
    int msg_iovlen; // I/O缓存区数组元素数量
    void* msg_control; // 辅助数据起始位置
    socklen_t msg_controllen; // 辅助数据字节数
    int msg_flags; // 等于recvmsg和sendmsg的flags参数,在调用过程中更新
};

辅助函数

获取地址

#include<sys/socket.h>
// 获取socketfd本端的地址信息,存到address,如果address长度大于address_len,将被截断
int getsockname(int sockfd,struct sockaddr*address,socklen_t*address_len);

// 获取socketfd远端的地址信息
int getpeername(int sockfd,struct sockaddr*address,socklen_t*address_len);

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

socketfd属性设置,option

#include<sys/socket.h>
int getsockopt(int sockfd,int level,int option_name,
				void*option_value,socklen_t*restrict option_len);
int setsockopt(int sockfd,int level,int option_name,
				const void*option_value,socklen_t option_len);

成功返回0,失败返回-1设置errno,记录一下option_name,后面用到结合具体实例分析

gethostbyname & gethostbyaddr

根据主机名称获取主机的完整信息、根据地址获取主机的完整信息,信息返回结构如下:

#include<netdb.h>
struct hostent
{
    char* h_name; /*主机名*/
    char** h_aliases; /*主机别名列表,可能有多个*/
    int h_addrtype; /*地址类型(地址族)*/
    int h_length; /*地址长度*/
    char** h_addr_list /*按网络字节序列出的主机IP地址列表*/
};

getservbyname & getservbyport

根据服务名称或端口号获取服务信息,从/etc/services获取信息,该文件中存放的是知名端口号和协议等信息。返回结构体如下:

#include<netdb.h>
struct servent
{
    char* s_name; /*服务名称*/
    char** s_aliases; /*服务的别名列表,可能有多个*/
    int s_port; /*端口号*/
    char* s_proto; /*服务类型,通常是tcp或者udp*/
};

getaddrinfo

可以认为是调用了gethostbyname和getservbyname

#include<netdb.h>
// hostname:可以是主机名或IP地址字符串
// service:可以接收服务名,也可以接收十进制端口号
// result指向返回结果的链表,结构为addrinfo
int getaddrinfo(const char* hostname,const char* service,const
struct addrinfo* hints,struct addrinfo** result);

addrinfo结构体:

struct addrinfo
{
int ai_flags; /*大部分设置hints参数*/
int ai_family; /*地址族*/
int ai_socktype; /*服务类型,SOCK_STREAM或SOCK_DGRAM*/
int ai_protocol; /*通常设置为0*/
socklen_t ai_addrlen; /*socket地址ai_addr的长度*/
char* ai_canonname; /*主机的别名*/
struct sockaddr* ai_addr; /*指向socket地址*/
struct addrinfo* ai_next; /*指向下一个sockinfo结构的对象*/
};

getaddrinfo结束后,释放result分配的堆内存

void freeaddrinfo(struct addrinfo* res);

getnameinfo

可以认为是调用了gethostbyaddr和getservbyport

#include<netdb.h>
// 返回的主机名存储在host,服务名存储在serv
int getnameinfo(const struct sockaddr *sockaddr,socklen_t addrlen,
	char* host,socklen_t hostlen,char *serv,socklen_t servlen,int flags);
gai_strerror

转换getnameinfo和getaddrinfo返回的错误码为可读的字符串

#include<netdb.h>
const char* gai_strerror(int error);

getaddrinfo和getnameinfo返回的错误码如下:

简单示例

testserver.cc,testserver 0.0.0.0 8889

#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>

#include <cassert>
#include <cstdio>
#include <iostream>
using namespace std;

int main(int argc, char *argv[]) {
  if (argc <= 2) {
    cout << "usage:" << argv[0] << " ip_address port_number" << endl;
    return 0;
  }

  const char *ip = argv[1];
  int port = atoi(argv[2]);
  struct sockaddr_in address, client_addr;
  address.sin_family = AF_INET;
  address.sin_port = htons(port);
  inet_pton(AF_INET, ip, &address.sin_addr);
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
  assert(ret != -1);
  ret = listen(sockfd, 2);
  assert(ret != -1);
  socklen_t client_addr_length = sizeof(client_addr);
  int conn =
      accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_length);
  if (conn < 0)
    cout << "connect error: " << errno << endl;
  else {
    string hello = "hello client";
    send(conn, hello.data(), sizeof(hello), 0);
    close(conn);
  }
  close(sockfd);
  return 0;
}

testclient.cc,/etc/hosts加入server的地址和主机名,testclient myserver

#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>

#include <cassert>
#include <iostream>

using namespace std;

int main(int argc, char* argv[]) {
  if (argc != 2) {
    cout << "usage: " << argv[0] << " hostname" << endl;
    return 0;
  }
  char* hostname = argv[1];
  // 获取主机信息
  struct hostent* hostinfo = gethostbyname(hostname);
  assert(hostinfo);

  /*
    获取server返回信息,自定义一个服务,
    编辑/etc/services, my	        8889/tcp
  */
  struct servent* servinfo = getservbyname("my", "tcp");
  assert(servinfo);
  cout << "myserver port is " << ntohs(servinfo->s_port) << endl;
  struct sockaddr_in address;
  address.sin_family = AF_INET;
  address.sin_port = servinfo->s_port;

  address.sin_addr = *(struct in_addr*)*hostinfo->h_addr_list;
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  int result = connect(sockfd, (struct sockaddr*)&address, sizeof(address));
  assert(result != -1);
  char buffer[128];
  result = recv(sockfd, buffer, sizeof(buffer), 0);
  cout << "resceived: " << result << endl;
  assert(result > 0);
  buffer[result] = '\0';
  cout << "server's message: " << buffer << endl;
  close(sockfd);
  return 0;
}

学习自:
《Linux高性能服务器编程》
《UNIX环境高级编程》
《UNIX系统编程》

posted @ 2023-11-14 23:45  佟晖  阅读(401)  评论(0编辑  收藏  举报