Socket通信API详细解析

关于UNIX环境高级编程和UNIX网络编程的学习记录和总结

前言

关于socket通信主要用到的API解析

API

网络字节序

以32位CPU为例,每次可装载4字节,在内存中排列的顺序会受到排列顺序的影响,可分为两类:
大端字节序(big endian):高位存在低地址, 低位存在高地址(网络通信采用),又称网络字节序;
小端字节序(little endian): 高位存在高地址, 低位存在低地址(计算机本地采用),又称主机字节序;

从本机向服务器(网络)发送数据时,首先要进行网络字节序的转换。
转换函数:

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);			//主要针对IP
uint16_t htons(uint16_t hostshort);			//主要针对port
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

h—>host,n—>network,l—>long, s—>short
所以,htonl—> host to network long,主机字节序转换为网络字节序

IP地址转换函数

由于如192.168.45.2这种的IP地址为点分十进制表示, 需要转化为uint32_t型, 有现成的函数(IPv4和IPv6都可以转换):

in_addr_t inet_addr(const char* strptr);
点分十进制字符串IPv4地址转化网络字节序整数表示的地址
失败返回INADDR_NONE

int inet_pton(int af,const char* src,void* dst);				//p表示点分十进制的ip,n网络上的二进制ip
const char* inet_ntop(int af,const char* src,char* dst,socklen_t size);

int inet_pton(int af,const char* src,void* dst);
将用字符串表示的IP地址(IPv4/IPv6)转换成网络字节序整数表示的地址。
参数:

  • af:
  1. AF_INET
  2. AF_INET6
  • src:传入参数, 待转换的点分十进制的IP地址;
  • dst:传出参数, 转换后符合网络字节序的IP地址;

返回值:

  • 成功返回1;
  • 若参2无效返回0(异常);
  • 失败返回-1

const char* inet_ntop(int af,const char* src,char* dst,socklen_t size)

参数:

  • af:
  1. AF_INET
  2. AF_INET6
  • src:传入参数, 待转换的网络字节序的IP地址;
  • dst:传出参数, 转换后的点分十进制IP地址, 是一块缓冲区;
  • size指定了缓冲区的大小;

返回值:

  • 成功返回dst指针;
  • 失败返回NULL指针, 设置errorno;

in_addr–>struct sockaddr_in cin; cin.sin_addr

int inet_aton(const char* cp,struct in_addr* inp);
同样完成点分十进制IPv4转化网络字节序,但将转化结果存储与参数inp指向的地址结构中
成功:1
失败:0

char *inet_ntoa(struct in_addr in); 不可重入
网络字节序转换点分十进制IPv4地址,函数内部用一个静态变量存储,函数返回值指向该静态内存。

socket地址结构sockaddr

通用:sockaddr结构体

struct sockaddr
{
	sa_family_t sa_family;	//地址族类型
	char sa_data[14];		
}

常见地址族与对应协议族

协议族地址族描述
PF_UNIXAF_UNIXUNIX本地与域协议族(可用于本地套接字)
PF_INETAF_INETTCP/IPv4
PF_INET6AF_INET6TCP/IPv6

sa_data无法完全容纳多数协议族地址值,所以对此结构体进行改进。

struct sockaddr
{
	sa_family_t sa_family;	//地址族类型
	unsigned long int __ss_align;		//内存对齐
	char __ss_padding[128-sizeof(__ss_align)];		
}

提供足够大空间存放地址值,内存对齐。
进一步改进使用linux专用socket地址结构体

//ivp4
struct sockaddr_in{
	sa_family_t		sin_family;		//地址族
	in_port_t		sin_port;		//端口号(网络字节序)
	struct in_addr	sin_addr;		//ipv4地址结构体
};
struct in_addr{
    uint32_t s_addr;				//ipv4地址(网络字节序)
};
//ipv6
struct sockaddr_in6{
	sa_family_t		sin6_family;		//地址族
	in_port_t		sin6_port;		//端口号(网络字节序)
	in_port_t 		sin6_flowinfo;	//流信息,应设置0
	struct in6_addr	sin6_addr;		//ipv6地址结构体
	u_int32_t sin6_scope_id;		//scope ID
};
struct in_addr{
    uint32_t s_addr;				//ipv4地址(网络字节序)
};

所有专用socket地址在使用时都需要强制转化为sockaddr类型

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */

tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
raw_socket = socket(AF_INET, SOCK_RAW, protocol);

socket()创建socket

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

1.domain指定使用的协议(IPv4或IPv6)

  • AF_INET;
  • AF_INET6;
  • AF_UNIX或AF_LOCAL;

2.type指定数据传输协议(流式或报式)

  • SOCK_STREAM;
  • SOCK_DGRAM;
  • SOCK_NONBLOCK; —>新创建socket设置非阻塞
  • SOCK_CLOEXEC; —>用fork调用创建子进程时在子进程中关闭该socket

3.指定代表协议(一般传0–>使用默认协议)

  • 流式以TCP为代表;
  • 报式以UDP为代表;

成功返回新套接字的fd, 失败返回-1并设置errno;

bind()命名socket

int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
给socket绑定地址结构(IP+PORT)
将addr指向的socket地址分配给未命名的sockfd文件描述符,addrlen指出该地址结构长度
成功:0
失败:-1,设置errno
EACCES—>被绑定地址是受保护地址(端口0-1023),只有超级用户能够访问
EADDRINUSE—>被绑定地址正在使用,如TIME_WAIT态的socket地址.

listen()设置监听上限

int listen(int sockfd,int backlog);
listen(设置最大连接数或者说能同时进行三次握手的最大连接数)

sockfd–仍然传入socket函数的返回值;

backlog–上限数值, 最大128;—>处于完全连接状态的socket上限

成功返回0, 失败返回-1并设置errno;
ECONNREFUSED监听队列长度超过backlog

accept()接受连接

accept(阻塞等待客户端建立连接, 成功的话返回一个与客户端成功连接的socket文件描述符):
每次调用accept()之前应该重新赋初值

int accept(int sockfd,sockaddr* addr,int socklen_t* addrlen);

sockfd–socket函数的返回值;

addr–传出参数, 成功与Sever建立连接的那个客户端的地址结构;

addrlen–传入传出参数;

​ socklen_t clit_addr_len=sizeof(addr);

​ 入: 调用者提供的缓冲区addr的长度以避免缓冲区溢出问题

​ 出: 客户端addr的实际大小;

返回值:

​ 成功: 返回能与Server进行通信的socket对应的文件描述符;

​ 失败: 返回-1并设置errno;

注意:accept只是从监听队列里取出连接,不论连接处于各种状态,也不关心任何网络状况的变化(发生异常也会接受连接)

connect()发起连接

connect(使用现有的socket与服务器建立连接)

int connect(int sockfd,const struct sockaddr* addr,socklen_t addrlen);

sockfd–socket函数返回值;

addr–传入参数, 服务器监听的地址结构;

addrlen服务器地址结构的长度;

返回值:

​ 成功返回0;sockfd唯一标识此连接

​ 失败返回-1并设置errno;
ECONNREFUSED目标端口不存在
ETIMEDOUT连接超时

如果不使用bind()函数绑定客户端的地址结构, 会采用**“隐式绑定”**;

关闭连接

int close(int fd);
将fd的引用计数-1,当fd引用计数为0关闭连接。
在多进程中,一次fork将父进程打开的socket引用计数+1,所以要在父进程和子进程中都调用close()才能关闭连接。

int shutdown(int sockfd, int howto);
立即终止连接

howto参数:

可选值含义
SHUT_RD关闭读的这一半,不能对该文件描述符读操作,且接收缓冲区的数据被丢弃
SHUT_WR关闭写,发送缓冲区的数据会在真正关闭连接之前全部发送出去,连接处于半关闭状态
SHUT_RDWR关闭读写

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

TCP数据读写

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
读取sockfd上的数据
buf、len指定读缓冲区的位置和大小
返回值:
成功返回读到实际长度
0 对端关闭
-1 出错,errno

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
往socket上写,
buf、len指定写缓冲区位置和大小
成功:写入长度
失败:-1,errno

关于flags参数:

在这里插入图片描述
flag参数只对当前send和recv调用生效。
可使用MSG_OOB设置带外数据。

UDP数据读写

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

recvfrom读取sockfd上的数据,src_addr发送端的socket地址,addrlen地址长度
sendto往sockfd上写入数据,dest_addr指定接收端socket地址

flags与上相同

若把recvfrom/sendto最后两个参数设置为NULL可用于面向链接的socket读写

通用数据读写

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
struct msghdr {
               void         *msg_name;       /* Optional address */
               socklen_t     msg_namelen;    /* Size of address */
               struct iovec *msg_iov;        /* Scatter/gather array 分散的内存块*/
               size_t        msg_iovlen;     /* # elements in msg_iov 分散内存块的数量*/
               void         *msg_control;    /* Ancillary data, see below 指向辅助数据的起始位置*/
               size_t        msg_controllen; /* Ancillary data buffer len 辅助数据的大小*/
               int           msg_flags;      /* Flags (unused) */
           };

struct iovec {
			void *iov_base;					//内存起始地址
			size_t iov_len;					//这块内存的长度
			};

msg_name指定对端socket地址结构,TCP:NULL(对方地址已经知道),
recvmsg分散读(scatter read): 数据被读取存放在msg_iovlen块分散的内存中,内存位置和长度由msg_iov指向数组指定。
sendmsg集中写(gather writer): msg_iovlen块分散内存中的数据一并被发送。


版权声明:本文为qq_43460315原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。