目前遇到2个陷阱:
- server端在受到异常退出后,想要再次启动发现bind函数进行系统报错:"Address already in use"
- accept函数虽然可以传出addr_len参数为addr结构体实际大小(16 字节),但是如果不事先初始化好addr_len参数的话,会导致accep函数拿不到当前连接客户端的IP地址和端口号。
先说“陷阱1”
如果先ctrl+c结束服务器端程序的话,再次启动服务器就会出现Address already in use这个错误,或者你的程序在正常关闭服务器端socket后还是有这个问题。
代码如下:
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#define PORT 8888//设置端口号
#define LEN 1024
#define LOCALIP "127.0.0.1"
int main()
{
int sock_fd = 0;//sock句柄
int connect_fd = 0;//accept返回的句柄
char buff[LEN] = {0};
struct sockaddr_in server;//下面用于存储服务器的基本信息:报文类型、端口、IP
struct sockaddr_in client;//可以通过accept函数的传出参数,得到客户端的报文类型、端口、IP配置
socklen_t socklen = 0;//socklen表明客户端sock套接字长度,accept函数会更新此数据
printf("socklen is :%d\n", socklen);
sock_fd = socket(AF_INET, SOCK_STREAM, 0);//赋值fd
if(sock_fd < 0)//sock初始化失败
{
perror("sock build");
return -1;
}
else//sock初始化成功
{
//初始化服务器信息:类型、端口、IP
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
inet_pton(AF_INET, LOCALIP, &server.sin_addr.s_addr);//点分十进制地址的(主机字节序)转换成(网络字节序)
if(bind(sock_fd, (struct sockaddr*)&server, sizeof(server)) < 0)//绑定sock配置失败
{
perror("bind build");
return -1;
}
else//绑定sock配置成功
{
//SOMAXCONN的大小可以通过cat /proc/sys/net/core/somaxconn查看,默认4kb(4096字节)
if(listen(sock_fd, SOMAXCONN) < 0)//监听失败
{
perror("listen build");
return -1;
}
else//监听成功
{
connect_fd = accept(sock_fd, (void*)&client, &socklen);
printf("sizeof(client) = %ld,&client is: %p,socklen is :%d\n", sizeof(client), &client, socklen);
if(connect_fd < 0)//连接失败
{
perror("connect");
}
else//连接成功
{
int ret;
char remoteip[20];
int remoteport = ntohs(client.sin_port);
inet_ntop(AF_INET, &client.sin_addr.s_addr, remoteip, sizeof(remoteip));
printf("22sizeof(client) = %ld,&client is: %p,socklen is :%d\n", sizeof(client), &client, socklen);
while(1)
{
ret = read(connect_fd, buff, LEN);
if(ret < 0)
{
perror("read");
break;
}
else if(ret == 0)
{
printf("write dead.\n");
break;
}
printf("收到来自IP[%s]Port[%d]的信息:%s\n",remoteip, remoteport, buff);
}
close(connect_fd);//关闭传输句柄
close(sock_fd);//关闭连接句柄
return 0;
}
}
}
}
}
整个程序是进行正常的句柄关闭的。
bind 函数普遍遭遇的问题是试图绑定一个已经在使用的端口。该陷阱是也许没有活动的套接字存在,但仍然禁止绑定端口(bind 返回 EADDRINUSE),它由 TCP 套接字状态 TIME_WAIT 引起。该状态在套接字关闭后约保留 2 到 4 分钟。在 TIME_WAIT 状态退出之后,套接字被删除,该地址才能被重新绑定而不出问题。
等待 TIME_WAIT 结束可能是令人恼火的一件事,特别是如果您正在开发一个套接字服务器,就需要停止服务器来做一些改动,然后重启。幸运的是,有方法可以避开 TIME_WAIT 状态。可以给套接字应用 SO_REUSEADDR 套接字选项,以便端口可以马上重用。
代码如下:
// 设置套接字选项避免地址使用错误
int on=1;
if((setsockopt(server_sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0)
{
perror("setsockopt failed");
exit(EXIT_FAILURE);
}
“陷阱2”
先来看accept函数的拆解:
- 包含头文件<sys/socket.h>
- 功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。
- 原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 参数:
- sockfd:服务器套接字
- addr:将返回对等方的套接字地址
- addrlen:返回对等方的套接字地址长度
- 返回值:成功返回非负整数,失败返回-1
坑的地方就在这个addrlen参数
先说结论:调用accept函数之前一定要初始化addrlen参数,其大小必须大于或等于struct sockaddr addr结构体的大小(也就是16字节)。
我一开始的代码如下:
nt main()
{
int sock_fd = 0;//sock句柄
int connect_fd = 0;//accept返回的句柄
char buff[LEN] = {0};
struct sockaddr_in server;//下面用于存储服务器的基本信息:报文类型、端口、IP
struct sockaddr_in client;//可以通过accept函数的传出参数,得到客户端的报文类型、端口、IP配置
socklen_t socklen = 0;//socklen表明客户端sock套接字长度,accept函数会更新此数据
printf("socklen is :%d\n", socklen);
sock_fd = socket(AF_INET, SOCK_STREAM, 0);//赋值fd
if(sock_fd < 0)//sock初始化失败
{
perror("sock build");
return -1;
}
else//sock初始化成功
{
//初始化服务器信息:类型、端口、IP
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
inet_pton(AF_INET, LOCALIP, &server.sin_addr.s_addr);//点分十进制地址的(主机字节序)转换成(网络字节序)
if(bind(sock_fd, (struct sockaddr*)&server, sizeof(server)) < 0)//绑定sock配置失败
{
perror("bind build");
return -1;
}
else//绑定sock配置成功
{
//SOMAXCONN的大小可以通过cat /proc/sys/net/core/somaxconn查看,默认4kb(4096字节)
if(listen(sock_fd, SOMAXCONN) < 0)//监听失败
{
perror("listen build");
return -1;
}
else//监听成功
{
connect_fd = accept(sock_fd, (void*)&client, &socklen);
发现获取的客户端IP地址和端口号是0.0.0.0 port:0,此时我在accept函数下打印了addrlen参数的数值,发现其也已经被accept函数变更为16了。
原来是系统内核会先通过addrlen原始的数值大小进行判断, 是否能够满足struct sockaddr addr结构体的数据存储,如果不满足,则放弃本次参数的传出(所以获得全0的客户端ip和端口号),在函数结束之前,在返回addrlen的数值为sizeof(struct sockaddr addr)也就是“16字节”。
所以虽然accept函数调用结束后,打印出来的addrlen从0变成了16,但是却没有成功保存客户端ip和端口。
以上是我在网络编程中遇见的2个小陷阱,希望能帮助到各位coder。