关于TCP状态转换,自行查阅《UNIX网络编程_卷1_套接字联网API_第3版》第二章第六节。
关于TIME_WAIT状态,自行查阅《UNIX网络编程_卷1_套接字联网API_第3版》第二章第七节。
server.c:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 9000 // 本服务器要监听的端口号,一般1024以下的端口很多都是周知端口,所以这里采用1024之后的数字做端口号
int main(int argc, char* const* argv)
{
int listenfd = socket(AF_INET, SOCK_STREAM, 0); // 创建服务器的socket套接字(文件描述符)
struct sockaddr_in serv_addr; // 服务器的地址结构体
memset(&serv_addr, 0, sizeof(serv_addr));
// 设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据
serv_addr.sin_family = AF_INET; // 选择协议族为IPV4
serv_addr.sin_port = htons(SERV_PORT); // 绑定我们自定义的端口号,客户端程序和服务器程序通讯时,就要往这个端口连接和传送数据
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本地所有的IP地址,INADDR_ANY表示一个服务器上所有的网卡(服务器可能不止一个网卡),多个本地ip地址都进行绑定端口号,进行侦听
int result;
result = bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); // 绑定服务器地址结构体
if (result == -1)
{
char* perrorinfo = strerror(errno);
printf("bind返回的值为%d,错误码为:%d,错误信息为:%s;\n", result, errno, perrorinfo);
return -1;
}
result = listen(listenfd, 32); // 第二个参数表示服务器可以积压的未处理完的连入请求总个数,客户端来一个未连入的请求,请求数+1,连入请求完成、c/s之间进入正常通讯后,请求数-1
if (result == -1)
{
char* perrorinfo = strerror(errno);
printf("listen返回的值为%d,错误码为:%d,错误信息为:%s;\n", result, errno, perrorinfo);
return -1;
}
int connfd;
const char* pcontent = "I sent sth to client!\n"; // 指向常量字符串区的指针
for (;;)
{
// 卡在这里,等客户端连接,客户端连入后,该函数走下去
// 注意这里返回的是一个新的socket(connfd),后续本服务器就用connfd和客户端之间收发数据,而原有的lisenfd依旧用于继续监听其他连接
connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
// 发送数据包给客户端
write(connfd, pcontent, strlen(pcontent)); // 注意第一个参数是accept()返回的connfd套接字
printf("本服务器给客户端发送了一串字符~~~~~~~~~~~!\n");
// 只给客户端发送一个信息,然后直接关闭套接字连接
close(connfd);
}
close(listenfd); // 实际上本范例走不到这里
return 0;
}
运行服务器程序,用 netstat -anp | grep -E 'State|9000'
命令观察到监听端口一直处在 LISTEN
状态,我们用两个客户端连接到服务器,服务器给每个客户端发送一串字符 “I sent sth to client!\n” 并关闭客户端,虽然这两个连接被 close 掉了,但是产生了两条 TIME_WAIT
状态的信息。
此时,我们杀掉服务器程序再重新启动,就会启动失败,bind() 函数返回失败。
server.c:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERV_PORT 9000 // 本服务器要监听的端口号,一般1024以下的端口很多都是周知端口,所以这里采用1024之后的数字做端口号
int main(int argc, char* const* argv)
{
int listenfd = socket(AF_INET, SOCK_STREAM, 0); // 创建服务器的socket套接字(文件描述符)
struct sockaddr_in serv_addr; // 服务器的地址结构体
memset(&serv_addr, 0, sizeof(serv_addr));
// 设置本服务器要监听的地址和端口,这样客户端才能连接到该地址和端口并发送数据
serv_addr.sin_family = AF_INET; // 选择协议族为IPV4
serv_addr.sin_port = htons(SERV_PORT); // 绑定我们自定义的端口号,客户端程序和服务器程序通讯时,就要往这个端口连接和传送数据
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本地所有的IP地址,INADDR_ANY表示一个服务器上所有的网卡(服务器可能不止一个网卡),多个本地ip地址都进行绑定端口号,进行侦听
// setsockopt():设置一些套接字参数选项
// 第二个参数:表示级别,和第三个参数配套使用,也就是说,如果第三个参数确定了,第二个参数也就确定了
// 第三个参数:允许重用本地地址
int reuseaddr = 1; // 开启
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuseaddr, sizeof(reuseaddr)) == -1)
{
char* perrorinfo = strerror(errno);
printf("setsockopt(SO_REUSEADDR)返回值为%d,错误码为:%d,错误信息为:%s;\n", -1, errno, perrorinfo);
}
int result;
result = bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); // 绑定服务器地址结构体
if (result == -1)
{
char* perrorinfo = strerror(errno);
printf("bind返回的值为%d,错误码为:%d,错误信息为:%s;\n", result, errno, perrorinfo);
return -1;
}
result = listen(listenfd, 32); // 第二个参数表示服务器可以积压的未处理完的连入请求总个数,客户端来一个未连入的请求,请求数+1,连入请求完成、c/s之间进入正常通讯后,请求数-1
if (result == -1)
{
char* perrorinfo = strerror(errno);
printf("listen返回的值为%d,错误码为:%d,错误信息为:%s;\n", result, errno, perrorinfo);
return -1;
}
int connfd;
const char* pcontent = "I sent sth to client!\n"; // 指向常量字符串区的指针
for (;;)
{
// 卡在这里,等客户端连接,客户端连入后,该函数走下去
// 注意这里返回的是一个新的socket(connfd),后续本服务器就用connfd和客户端之间收发数据,而原有的lisenfd依旧用于继续监听其他连接
connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
// 发送数据包给客户端
write(connfd, pcontent, strlen(pcontent)); // 注意第一个参数是accept()返回的connfd套接字
printf("本服务器给客户端发送了一串字符~~~~~~~~~~~!\n");
// 只给客户端发送一个信息,然后直接关闭套接字连接
close(connfd);
}
close(listenfd); // 实际上本范例走不到这里
return 0;
}
setsockopt(SO_REUSEADDR):用在服务器端,socket() 创建之后,bind() 之前。
SO_REUSEADDR
的能力:
SO_REUSEADDR
允许启动一个监听服务器并捆绑其端口,即使以前建立的将端口用作它们的本地端口的连接仍旧存在。也就是说,即便 TIME_WAIT 状态存在,服务器 bind() 也能成功。SO_REUSEADDR
允许同一个端口上启动同一个服务器的多个实例,只要每个实例捆绑一个不同的本地 IP 地址即可。SO_REUSEADDR
允许单个进程捆绑同一个端口到多个套接字,只要每次捆绑指定不同的本地 IP 地址即可。SO_REUSEADDR
允许完全重复的绑定:当一个 IP 地址和端口已经绑定到某个套接字上时,如果传输协议支持,同样的 IP 地址和端口还可以绑定到另一个套接字上,一般来说本特性仅支持 UDP
套接字。TCP
服务器都应该指定本套接字选项,以防止当套接字处于 TIME_WAIT 时 bind() 失败的情形出现。