关于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:
- AF_INET
- 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:
- AF_INET
- 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_UNIX | AF_UNIX | UNIX本地与域协议族(可用于本地套接字) |
| PF_INET | AF_INET | TCP/IPv4 |
| PF_INET6 | AF_INET6 | TCP/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块分散内存中的数据一并被发送。