前记:因项目的需求,需要写一个linux TCP服务器,首先想到的是拿来主意,于是翻出了工作十多年的各个项目的服务器,发现每个服务器都有点不合现在的需求,于是打算自己动手写一个,在写时发现有些基本的套接字的内容不太熟了,估计还是平时拿来主义影响了代码的动手能力,于是开始查找资料,发现很多资料或代码只能用来示例,用来做项目还是不行,于是准备自己写一个。方便后面还需要写TCP服务器时不需要到处查找资料。详细代码请订阅后私信入群获取。
当前项目的服务器因去掉了心跳,所以需要判断客服端是否已断开,当客服端断开后我们需要释放掉相关资源和反初始化,再次进行等待客服端的连接。代码的重心也就在客服端断开的判定和客服端断开后的资源释放和反初始化。因协议的问题,本项目的数据是没有协议头,并且不同指令的长度也不一样,因此在数据接收和处理上也是跟通用的具有协议头的方式有些差别。
一、需要使用到的知识
1、SO_REUSEADDR:
重用本地地址。未设置此项前,若服务端开启后,又关闭,此时sock处于TIME_WAIT状态,与之绑定的socket地址不可重用,而导致再次开启服务端失败。经过setsockopt设置之后, 即使处于TIME_WAIT些状态也可以立即被重用。
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2、SO_RCVBUF 和 SO_SNDBUF
TCP接收缓冲区和发送缓冲区的大小。即使我们设置了这两项的大小时, 系统都会自动将其加倍, 并且不得小于某个最小值。设置他们的目的是确保一个TCP连接拥有足够多的空闲缓冲区来处理拥塞。
3、struct sockaddr和struct sockaddr_in 结构体
struct sockaddr {
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
struct sockaddr_in{
//sin_port和sin_addr都必须是网络字节序(NBO),一般可视化的数字都是主机字节序(HBO)
sa_family_t sin_family; //地址簇
uint16_t sin_port; //16位TCP/UDP 端口号
struct in_addr sin_addr; //32 位IP地址
char sin_zero[8]; //不使用
};
二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。
sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
下面示例代码展示他们的关系和使用:
struct sockaddr_in serverAddr;
memset(&serverAddr, 0,sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(7048);
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr))
在bind 使用中,我们先使用struct sockaddr_in serverAddr 来建立所需的信息,比如端口,地址等,最后我们在bind 中来进行转换(struct sockaddr *)&serverAddr 。
4、htons() ,htonl()和 inet_addr()
htons()作用是将端口号由主机字节序转换为网络字节序的整数值。(host to net)
htonl()作用和htons()一样,不过它针对的是32位的(long),而htons()针对的是两个字节,16位的(short)
inet_addr()作用是将一个IP字符串转化为一个网络字节序的整数值,用于sockaddr_in.sin_addr.s_addr。
5、select 函数
int select(int nfds, fd_set* readset, fd_set* writeset, fe_set* exceptset, struct timeval* timeout);
nfds 需要检查的文件描述字个数
readset 用来检查可读性的一组文件描述字。
writeset 用来检查可写性的一组文件描述字。
exceptset 用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)
timeout 超时,填NULL为阻塞,填0为非阻塞,其他为一段超时时间
recv, recvfrom函数都是阻塞的函数,当函数不能成功执行的时候,程序就会一直阻塞在这里,无法执行下面的代码。这时就需要用到非阻塞的编程方式,使用select函数就可以实现非阻塞编程。
select函数是一个轮循函数,循环询问文件节点,可设置超时时间,超时时间到了就跳过代码继续往下执行。
6、FD_ZERO、FD_SET、FD_CLR 和FD_ISSET
FD_ZERO(fd_set *fdset):清空fdset与所有文件句柄的联系。
FD_SET(int fd, fd_set *fdset):建立文件句柄fd与fdset的联系。
FD_CLR(int fd, fd_set *fdset):清除文件句柄fd与fdset的联系。
FD_ISSET(int fd, fdset *fdset):检查fdset联系的文件句柄fd是否可读写,>0表示可读写。
这几个函数主要用来跟select 进行配合使用。for eg:
FD_ZERO(&fds);
FD_SET(sockfd, &fds);
FD_ISSET(sockfd, &fds)
7、socket、setsockopt、bind、listen、send、recv
int socket(int protofamily, int type, int protocol);//返回sockfd
protofamily:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
int setsockopt( int socket, int level, int option_name,const void *option_value, size_t ,ption_len);
socket:是套接字描述符。
level:是被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET。
option_name:指定准备设置的选项,option_name可以有哪些取值,这取决于level
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。
addrlen:对应的是地址的长度。
int listen(int sockfd, int backlog);
sockfd:要监听的socket描述字
backlog:socket可以排队的最大连接个数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd
sockfd:监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联.
addr:这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
addrlen:接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
成功:返回接收数据字节数
失败:返回-1
sockfd:通信文件描述符
buf: 应用缓存,用于存放接收的数据
len:buf 大小
flags:常设为0,此时为阻塞接收
ssize_t send(int sockfd, void *buf, size_t len, int flags);
sockfd:通信文件描述符
buf: 应用缓存,用于存放发送的数据
len:发送数据的字节数
flags:常设为0
二、解析数据机制
因为无协议头,但是前8各字节都是固定的格式,其实也可以当作协议头的模式,后面的数据内容根据获取的长度来进行计算。在recv 数据的时候分作两部分,一部分为前面八个固定的字节内容,一部分为后面根据实际长度来获取。另需要注意数据的封装。