现代pc大多小端,称为主机字节序
不同主机可能字节序不同,为了统一,网络间传输统一采用大端,称网络字节序
#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 long int ntohs(unsigned short int netlong);
通用socket地址 struct sockaddr
,地址族类型(如AF_INET)常与协议族类型(如PF_INET)一一对应
由于不好用,有了专用socket地址,IPv6略
专用socket地址在实际使用时都需转成通用socket地址类型,强转即可
#include <sys/un.h>
struct sockaddr_un{
sa_family_t sa_family; // 地址族类型 AF_UNIX
char sun_path[108];
};
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; // IP地址 网络字节序
};
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr); // 点分十进制转网络字节序,失败返回INADDR_NONE
int inet_aton(const char* cp, struct in_addr* inp); // 转网络字节序,结果保存inp,失败返回0
char *inet_ntoa(struct in_addr in); // 网络字节序转点分十进制,结果指向在静态变量
对服务器端来说,需要绑定socket地址,只有绑定后,客户端才知道如何连接它
服务端被动监听,客户端主动连接
#include <sys/types.h>
#include <sys/socket.h>
// domain 指定该使用哪个底层协议族 PF_INET -> IPv4
// type 指定服务类型 SOCK_STREAM 流服务 SOCK_UGRAM 数据报服务
// 自内核2.6.17起,可与 SOCK_NONBLOCK SOCK_CLOEXEC 相与
// protocol 设置0,使用默认协议
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
// backlog 提示内核监听队列的最大长度 典型值是5 (处于ESTABLISHED状态)
// 完整连接最多有backlog + 1个
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
// 成功返回新连接的socketfd
// accpet只是从内核监听队列中取出连接,而不论连接处于何种状态
对客户端来说,只需socket,connect
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
// 成功返回0,可以通过读写sockfd来与服务器通信
#include <unistd.h>
int close(int fd); // close系统调用并非总是立即关闭一个连接,而是将fd引用计数-1
// 多进程中,一次fork,默认将父进程的socket引用计数+1
#include <sys/socket.h>
int shutdown(int sockfd, int howto); // 专为网络编程设计,立即关闭sockfd
// howto可以选择如何关闭全双工本端 如SHUT_RD,SHUT_RDWR
#include <sys/types.h>
#include <sys/socket.h>
// recv 成功返回读到的数据长度,可能小于期望长度,需多次recv
// 返回0 通信对方已关闭连接; 失败返回-1
ssize_t recv(int sockfd, void *buf, size_t len, int flags); // flags通常设为0
// flags: MSG_PEEK 窥探读缓存中的数据,此次读操作不会删除读缓冲区中这些数据
// flags: MSG_OOB 紧急数据,设置后,不带此标志的正常数据将不会被recv一次读空
// flags只对当前调用生效,永久生效使用setsockopt
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
因UDP无连接,每次接收需指定发送方socket地址,每次发送亦需指定接收方socket地址
ps. recvfrom/sendto也可以用于面向连接的socket通信,只需把后2参数设置为NULL**(待验证)**
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags,
struct sockaddr* src_addr, socklent_t* addrlen);
ssizt_t sendto(int sockfd, const void* buf, size_t len, int flags,
const struct sockaddr* dest_addr, socklen_t addrlen);
适用于TCP/UDP
对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,称分散读
对sendmsg而言,数据将被一并发送,称为集中写
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
struct msghdr{
void* msg_name; // socket地址 -- 对TCP来说设置NULL
socklen_t msg_namelen; // socket地址长度
struct iovec* msg_iov; // 分散的内存 -- 结构体如下
int msg_iovlen; // 分散内存块数量
void* msg_control; // 辅助数据的起始位置 -- 可用于进程间传递文件描述符
socklen_t msg_controllen; // 辅助数据长度
int msg_flags; // 无需设定,自动复制函数中的flags
};
struct iovec{
void* iov_base; // 内存起始地址
size_t iov_len; // 长度
};
#include <sys/socket.h>
int sockatmark(int sockfd); // 判断sockfd是否处于带外标记,是1否0
// 获取本端/对端socket地址,并存于传出参数,成功0失败-1
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);
如果说fcntl是控制文件描述符属性的通用POSIX方法,那么getsockopt/setsockopt就是专门用来控制socket文件描述符属性的方法。
#include <sys/socket.h>
// level 指定要操作的协议 SOL_SOCKET, IPPROTO_IP, IPPROTO_TCP等
// option_name 操作名,如 SO_REUSEADDR等
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);
一般设置紧跟socket之后
SO_REUSEADDR 可以强制使用处于TIME_WAIT状态的连接占用的地址
经过设置地址可重用后,即使sockfd处于TIME_WAIT状态,socket地址可立即被重用。此外还可以修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle来快速回收被关闭的socket。
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &resue, sizeof(resue));
SO_RCVBUF, SO_SNDBUF 设置TCP接收和发送缓冲区大小,值通过option_value传入
不过,系统会将我们设置的值加倍,并不小于某个最小值(TCP接收缓冲区最小256字节,发送缓冲区2048字节),这样的目的是有足够缓冲区来处理拥塞。此外还可以修改内核参数/proc/sys/net/ipv4/tcp_rmem和tcp_wmem,这时你改多大就是多大了~
SO_RCVLOWAT, SO_SNDLOWAT 低水位标记,一般被IO复用用来判断socket是否可读或可写,当TCP接收缓冲区可读数据总数大于低水位标记时才认为sockfd可读;当TCP发送缓冲区中空闲空间大于低水位标记,才往sockfd里写数据。默认低水位标记均为1字节。
SO_LINGER 留坑待补
socket的两个信息ip和port都是数值表示不方便记忆
#include <netdb.h>
struct hostent* gethostbyname(const char* name);
// addr 点分十进制ip
// type 地址族 AF_INET
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
struct hostent{
char* h_name; // 主机名
char** h_aliases; // 主机别名列表
int h_addrtype; // 地址族
int h_length; // 长度
char** h_addr_list; // 网络字节序列出主机ip地址列表
};
#include <netdb.h>
// 实际上是查找/etc/services文件来获取服务信息
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
struct servent{
char* s_name;
char** s_aliases;
int s_port;
char* s_proto;
};
#include <netdb.h>
int getaddrinfo(......); // 通过主机名获取ip地址,通过服务名称获得端口号
int getnameinfo(......); // 通过socket地址获得以字符串表示的主机名和服务名