当前位置: 首页 > 工具软件 > SAEA.Socket > 使用案例 >

socketAPI

穆德海
2023-12-01

Socket地址API

主机字节序与网络字节序

现代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地址与专用Socket地址

通用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地址		网络字节序
};

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基础API

四部曲socket,bind,listen,accept

对服务器端来说,需要绑定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

TCP读写

#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读写

因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);

socket选项

如果说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 留坑待补

网络信息API

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地址获得以字符串表示的主机名和服务名
 类似资料:

相关阅读

相关文章

相关问答