文章目录
一.Socket
1.1 介绍
- Socket(套接字)是一种进程间通信方式,和其他进程间通信方式(管道、共享内存等)不同的是Socket可以在不同主机之间的进程通信
- 一个完整的socket使用唯一的五元组进行标识:
{协议、本地地址、本地端口、远程地址、远程端口}
1.2 socket数据结构
- 用于ip地址与端口管理
struct sockaddr {
unsigned short sin_family; /*地址族*/
char sa_data[14]; /*14 字节的协议地址,包含该 socket 的 IP 地址和端口号。 */
};
struct in_addr {
unsigned long int s_addr; /* 32 位 IPv4 地址,网络字节序 */
};
struct sockaddr_in {
short int sin_family; /*地址族*/
unsigned short int sin_port; /*端口号*/
struct in_addr sin_addr; /*IP 地址*/
unsigned char sin_zero[8]; /*填充 0 以保持与 struct sockaddr 同样大小*/
};
#include <netinet/in.h>
//sin_family: AF_INET-->IPv4协议 AF_INET6-->IPv6协议
1.3 socket编程系统函数
1.3.1 网络字节序与主机字节序的转化
- 端口转换
#include <arpa/inet.h>
uint16_t htons(uint16_t host); // Host to Network Long
uint32_t htonl(uint32_t host); // Network to Host Long
uint16_t ntohs(uint16_t net); // Host to Network Short
uint32_t ntohl(uint32_t net); // Network to Host Short
- ipv4地址转换
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// IPv4 的函数原型:
// 点分十进制数的IP地址字符串转32位的网络字节序的二进制值
// 成功返回1,不成功返回0
int inet_aton(const char *straddr, struct in_addr *addrptr);
// 功能同inet_aton
// in_addr_t就是unsigned long int 代表s_addr
in_addr_t inet_addr(const char *straddr);
in_addr_t inet_network(const char *straddr);
// 32位的网络字节序的二进制值转IP地址字符串
char *inet_ntoa(struct in_addr inaddr);
- ipv6地址转换(兼容ipv4)
#include <arpa/inet.h>
/**
* @brief This function converts the character string src into a network address structure
in the af address family
函数 inet_pton 跟 inet_aton 实现的功能类似,只是多了 family 参数
* @param family AF_INET表示 IPv4 协议,AF_INET6表示 IPv6 协议
* @param src
* @param dst
* @return 成功返回1,src无效返回0,af无效返回-1
*/
int inet_pton(int family, const char *src, void *dst);
/**
* @brief 函数 inet_ntop 跟 inet_ntoa 类似,其中 len 表示表示转换之后的长度(字符串的长度)
* @param af AF_INET表示 IPv4 协议,AF_INET6表示 IPv6 协议
* @param src
* @param dst
* @return 成功返回指向dst的非空指针
*/
const char *inet_ntop(int af, const void *src, char *dst, socklen_t len);
1.3.2 客户端/服务端编程流程函数
#include <sys/socket.h>
#include <sys/types.h>
/**
* @brief 申请套接字
* @param domain 域名:一般有如下类型
AF_UNIX: Local communication
AF_INET: IPv4 Internet protocols
AF_INET6: IPv6 Internet protocols
* @param type 指明连接语义:SOCK_STREAM 字节流TCP、SOCK_DGRAM 数据报UDP
* @param protocol 指定 socket 所使用的传输协议编号,通常为0
* @return file descriptor,失败返回-1
*/
int socket(int domain, int type, int protocol);
/**
* @brief 绑定IP地址与端口
* @param sockfd 套接字
* @param my_addr
* @param addrlen
* @return 成功则返回 0,失败返回-1
*/
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
/**
* @brief 监听端口
* @param sockfd 套接字
* @param backlog 指定同时能处理的最大连接要求,通常为 10 或者 5。最大值可设至 128
* @return 成功则返回 0,失败返回-1
*/
int listen(int sockfd,int backlog);
/**
* @brief 监听端口
* @param sockfd 套接字
* @param addr为结构体指针变量,系统会把远程主机的信息(远程主机的地址和端口号信息)保存到这个指针所指的结构体中
* @param addrlen表示结构体的长度,为整型指针
* @return 成功则返回连接fd,失败返回-1
*/
int accept(int sockfd, struct sockaddr * addr, int * addrlen);
/**
* @brief 从fd中接收信息,只是把数据从内核缓冲区复制到了应用缓冲区. 数据的接收是靠内核(TCP协议栈)完成的
* @param sockfd 套接字
* @param buf 缓冲区
* @param len 缓冲区的长度
* @param flags MSG_DONTROUTE--不查找路由表
MSG_OOB--接受或发送带外数据
MSG_PEEK--查看数据,并不从系统缓冲区移走数据
MSG_WAITALL--等待任何数据
* @return 成功则返回实际接收到的字符数,可能会少于你所指定的接收长度。失败返回-1
*/
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 和recv的区别是 src_addr接收对端的地址信息
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
/**
* @brief 对fd中发送信息,只是把数据从应用层缓冲区复制到了内核缓冲区, 数据的发送时靠内核(TCP协议栈)完成的
* @param sockfd 套接字
* @param buf 缓冲区
* @param len 缓冲区的长度
* @param flags 同上,具体可以查阅man文档,man send
* @return 成功则返回实际传送出去的字符数,可能会少于你所指定的发送长度。失败返回-1,缓冲区满返回-1
*/
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 若文件顺利关闭则返回 0,发生错误时返回-1
#include <unistd.h>
int close(int fd);
/**
* @brief 通过ip地址及端口连接服务器
* @param sockfd 套接字
* @param serv_addr 结构体指针变量,存储着远程服务器的 IP 与端口号信息
* @param addrlen 结构体变量的长度
* @return 成功则返回1 失败返回-1
*/
#include <sys/socket.h>
int connect(int sockfd,struct sockaddr *serv_addr,int addrlen);
1.3.3 对socket的设置
#include <sys/socket.h>
#include <sys/types.h>
// 成功则返回 0,失败返回-1
int getsockname(int peerfd, struct sockaddr *addr, socklen_t *addrlen); //解析本端IP和port
int getpeername(int peerfd, struct sockaddr *addr, socklen_t *addrlen); //解析对端IP和port
/**
* @brief 获得设置信息和设置socket所引用的文件描述符sockfd,必须要放在 bind 之前调用
* @param sockfd 套接字
* @param level 选项定义的层次;支持 SOL_SOCKET、 IPPROTO_TCP、 IPPROTO_IP 和 IPPROTO_IPV6
* @param optname 需设置的选项
* @param optval 指针,指向存放选项值的缓冲区
* @param optlen 缓冲区长度
* @return 成功则返回1 失败返回-1
*/
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
- 一些具体的操作如下:
// 如果在已经处于 ESTABLISHED 状态下的 socket(一般由端口号和标志符区分)
// 调用 close(socket)(一般不会立即关闭而经历 TIME_WAIT 的过程)后想继续重用该 socket
int reuse=1;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)&reuse,sizeof(int));
// 如果要已经处于连接状态的 socket 在调用 close(socket) 后强制关闭,不经历 TIME_WAIT 的过程
int reuse=0;
setsockopt(s,SOL_SOCKET ,SO_DONTLINGER,(const char*)&reuse,sizeof(int));
// 对端口的复用(意味着linux内核可以在同一台物理主机上实现系统级别的负载均衡)
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(int));
// 在 send(),recv()过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限
int nNetTimeout=1000; // 1 秒
// 发送时限
setsockopt(socket, SOL_S0CKET,SO_SNDTIMEO, (char *)&nNetTimeout,sizeof(int));
// 接收时限
setsockopt(socket, SOL_S0CKET,SO_RCVTIMEO, (char *)&nNetTimeout,sizeof(int));
• SO_REUSEPORT使用场景:linux kernel 3.9 引入了最新的SO_REUSEPORT选项,使得多进程或者多线程创建多个绑定同一个ip:port的监听socket,提高服务器的接收链接的并发能力,程序的扩展性更好;此时需要设置SO_REUSEPORT
• SO_REUSEADDR的使用场景:当服务端出现timewait状态的链接时,确保server能够重启成功
1.3.4 对文件描述符fd的设置
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
// 例如设置fd使用非阻塞IO
int flags = fcntl(fd, FD_GETFL, 0);
int ret = fcntl(fd, FD_SETFL, O_NONBLOCK | flags);
二.函数调用及TCP状态转移
- 简易同步操作的服务端和客户端的函数调用流程及TCP转移
- 1.关于TCP连接的全连接/半连接队列可以参考:TCP的全连接和半连接队列
- 2.蓝色为TCP状态变化
三.参考资料
- [1] TCP的全连接和半连接队列