五:优雅断连 & 域名<=>IP & 套接字多种选项

本文介绍了如何优雅地断开TCP连接,包括基于TCP的半关闭机制、使用shutdown函数、以及在Windows平台的应用。同时涵盖了DNS系统的工作原理,包括域名到IP地址的转换和网络通信中的套接字选项如SO_REUSEADDR和TCP_NODELAY的使用。
摘要由CSDN通过智能技术生成

1 优雅地断开套接字连接

1.1 基于TCP的半关闭

TCP断开连接过程比建立连接过程更重要,因为连接过程一般不会出问题,但是断开连接过程有可能发生预想不到的情况,所以应该了解半关闭(Half-close)。

  • 单方面断开带来的问题
    Linux的close函数和Windows的closesocket函数意味着完全断开连接,既不能传输数据,也不能接收。因此,一些情况下,某一方单独断开连接显得不太优雅。例如:

    主机A和主机B进行通信,A向B发送完数据后,调用close断开连接,此时A将无法在发送和接收数据,那么B发送给A的数据也只能销毁了。

  • 套接字和流
    两台主机通过套接字建立连接后进行可交换数据状态,又称“流形成的状态”。即可把建立套接字后可交换数据的状态看作一种流。流是单方向的,所以一个套接字有两个流(输入和输出)。

    可以看到主机的输入流与另一主机的输出流相连,主机的输出流与另一主机的输入流相连。

  • 针对优雅断开的shutdown函数

#include <sys/socket.h>
/**
* @param[2] : howto 传递断开方式信息
* 	可选值如下:
* 	SHUT_RD :断开输入流,无法接收数据
* 	SHUT_WD :断开输出流,无法发送数据
* 	SHUT_RDWR:同时断开IO流
*/
int shutdown(int sock, int howto);

有了半关闭我们知道对方是否关闭便可以做出更加效率的操作,不用傻等对面消息了。

  • 基于半关闭的文件传输程序

file_server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUF_SIZE 30  //C语言数组只能是常量,而不是const只读变量
void ErrorHandler(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char* argv[]) {
    int servSd, clntSd;
    FILE* fp;
    char buf[BUF_SIZE];
    int readCnt;

    struct sockaddr_in servAddr, clntAddr;
    socklen_t clntAddrSz;

    if (argc != 2) {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    fp = fopen("file_server.c", "rb");
    servSd = socket(PF_INET, SOCK_STREAM, 0);

    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAddr.sin_port = htons(atoi(argv[1]));

    bind(servSd, (struct sockaddr*)&servAddr, sizeof(servAddr));
    listen(servSd, 5);

    clntAddrSz = sizeof(clntAddr);
    clntSd = accept(servSd, (struct sockaddr*)&clntAddr, &clntAddrSz);

    while (1) {
        readCnt = fread((void*)buf, 1, BUF_SIZE, fp); 
        if (readCnt < BUF_SIZE) {
            write(clntSd, buf, readCnt);
            break;
        }
        write(clntSd, buf, BUF_SIZE); //传输文件数据
    }

    shutdown(clntSd, SHUT_WR);
    read(clntSd, buf, BUF_SIZE);
    printf("Messsage from client: %s \n", buf);

    fclose(fp);
    close(clntSd);
    close(servSd);
    return 0;
}

file_clinet.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sd;
	FILE *fp;
	
	char buf[BUF_SIZE];
	int read_cnt;
	struct sockaddr_in serv_adr;
	if(argc!=3) {
		printf("Usage: %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	fp=fopen("receive.dat", "wb");
	sd=socket(PF_INET, SOCK_STREAM, 0);   

	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_adr.sin_port=htons(atoi(argv[2]));

	connect(sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
	
	while((read_cnt=read(sd, buf, BUF_SIZE ))!=0) //直到收到EOF
		fwrite((void*)buf, 1, read_cnt, fp);
	
	puts("Received file data");
	write(sd, "Thank you", 10);
	fclose(fp);
	close(sd);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

1.2 基于win的实现

windows平台同样使用shutdown函数完成半关闭,只是传递的参数名有所不同。

#include <winsock2.h>

/**
* @param[1]: 要断开的套接字句柄
* @param[2]: 断开方式
* 			SD_RECIEVE:断开输入流
* 			SD_SEND:	断开输出流
* 			SD_BOTH:	同时断开IO
* @return success: 0; fail: SOCKET_ERROR
*/
int shutdown(SOCKET sock, int howto);

链接: win实现

2 域名和网络系统

2.1 域名系统

DNS是对IP地址和域名进行香花转换的系统,其核心是DNS服务器。

什么是域名

      提供网络服务的服务器端也是通过IP地址进行区分的,但是几乎不可能以非常难记的IP地址形式交换服务器端地址信息。因此,将容易记、易表述的域名分配并取代IP地址。

DNS服务器

    在浏览器地址栏输入Naver网站的IP地址22.122.195.5即可浏览Naver网站主页。但我们通常输入Naver网站的域名www.naver.com访问网站。二者之间有何区别?
    从进入Naver网站主页这一结果看,没有区别,但是接入过程不同。域名是赋予服务器端的虚拟地址,而非实际地址。因此需要将虚拟地址转化为实际地址。这时DNS便发挥作用。
    具体过程参考:链接: link

2.2 IP地址和域名系统之间的转换

域名系统必要性:IP地址比域名发生变更的概率要高

  1. 利用域名获取IP地址
#include <netdb.h>

/**
*@return 成功返回结构体指针,失败返回NULL指针
*/
struct hostent* gethostbyname(const char* hostname);

struct hostent
{
	char* h_name;  //官方域名
	char** h_aliases; //其他域名
	int h_addrtype;  //如果是IPv4,则变量存有AF_INET
	int h_length;    //保存IP地址长度,IPv4是4字节,IPv6是16字节 
	char** h_addr_list;  //以数组形式保存域名对应的IP地址
						//考虑到通用性,而不是只给IPv4用,所以采用char*而不是in_addr*, 
						//又因为此时void*还没标准化,所以采用char*更通用。
}
  • 获取百度ip的例子
    在这里插入图片描述
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>

void ErrorHandler(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char* argv[]) {
    struct hostent* host;
    if (argc != 2) {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    host = gethostbyname(argv[1]);
    if (!host) {
        ErrorHandler("gethost... error");
    }

    printf("official name : %s\n", host->h_name);
    for (int i = 0; host->h_aliases[i]; i++) {
        printf("Alisea %d: %s \n", i+1, host->h_aliases[i]);
    }
    printf("Address type: %s \n", (host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
    for (int i = 0; host->h_addr_list[i]; i++) {
        printf("IP Adddr %d: %s \n", i+1, inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));
    }
    return 0;
}
  1. 利用IP地址获取域名
#include <netdb.h>

struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);

腾讯的DNS服务器IP示例
在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>

void ErrorHandler(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char**argv) {
    if (argc != 2) {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }
    struct hostent *host;
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_addr.s_addr = inet_addr(argv[1]);
    host = gethostbyaddr((char*)&addr.sin_addr, 4, AF_INET);
    if (!host) {
        ErrorHandler("get host...error");
    }
    printf("official name : %s\n", host->h_name);
    for (int i = 0; host->h_aliases[i]; i++) {
        printf("Alisea %d: %s \n", i+1, host->h_aliases[i]);
    }
    printf("Address type: %s \n", (host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
    for (int i = 0; host->h_addr_list[i]; i++) {
        printf("IP Adddr %d: %s \n", i+1, inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));
    }
    return 0;
}

3 套接字多种可选项

3.1 套接字可选项与IO缓冲大小

  • 套接字多种可选项
    有时需要更改套接字特性,下表是一部分
    在这里插入图片描述
    在这里插入图片描述
    从表中看出,套接字可选项是分层的。IPPROTO_IP层可选项是IP协议相关事项,IPPROTO_TCP层可选项是TCP协议相关的事项,SOL_SOCKET层是套接字相关的通用可选项。
  • getsockopt & setsockopt
#include <sys/socket.h>

/**
* @param[1] sock 查看选项套接字的文件描述符
* @param[2] level 要查看的可选项的协议层
* @param[3] optname 要查看的可选项名
* @param[4] optval 保存查看结果的缓冲地址值
* @param[5] optlen 向第四个参数传递的缓冲大小
* @retval 成功0, 失败-1
*/
int getsockopt(int sock, int level, int optname, void* optval, socklen_t *optlen);

/**
* @param[1] sock 查看选项套接字的文件描述符
* @param[2] level 要查看的可选项的协议层
* @param[3] optname 要查看的可选项名
* @param[4] optval 保存查看结果的缓冲地址值
* @param[5] optlen 向第四个参数传递的缓冲大小
* @retval 成功0, 失败-1
*/
int setsockopt(int sock, int level, int optname, const void* optval, socklen_t *optlen);
  • sock_type.c
    在这里插入图片描述
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>

void ErrorHandler(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char** atgv) {
    int tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
    int udp_sock = socket(PF_INET, SOCK_DGRAM, 0);
    int sockType;
    socklen_t optlen = sizeof(sockType);

    int state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void*)&sockType, &optlen);
    if (state == -1) {
        ErrorHandler("getsockopt error");
    }
    printf("Socket type one: %d \n", sockType);

    state = getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void*)&sockType, &optlen);
    if (state == -1) {
        ErrorHandler("getsockopt error");
    }
    printf("Socket type two: %d \n", sockType);

    return 0;
}

注:套接字类型(tcp/udp)只能在创建时决定,后续不能更改。

  • SO_SNDBUF & SO_RECVBUF
    SO_RECVBUF是输入缓冲大小相关可选项,SO_SNDBUF是输出缓冲区大小相关可选项,这俩既可以读取,也可以更改。
    在这里插入图片描述

注:系统不能放任你修改缓冲区,所以要设置一个合理的值。

示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>

void ErrorHandler(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char** argv) {
/*--------------------------修改前----------------------------------------------*/
    int sndBuf;
    int len = sizeof(sndBuf);
    int sock = socket(PF_INET, SOCK_STREAM, 0);
    int state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&sndBuf, &len);
    if (state) {
        ErrorHandler("getsockopt error");
    }

    int recvBuf;
    len = sizeof(recvBuf);
    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&recvBuf, &len);
    if (state) {
        ErrorHandler("getsockopt error");
    }

    printf("input buffer size : %d, output buffer size : %d \n", recvBuf, sndBuf);

/*------------------修改后--------------------------------------------------------*/
    sndBuf = 1024*30;
    recvBuf = 1024*30;

    state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&recvBuf, sizeof(recvBuf));
    if (state) {
        ErrorHandler("setsockopt error");
    }

    state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&sndBuf, sizeof(sndBuf));
    if (state) {
        ErrorHandler("setsockopt error");
    }

    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&sndBuf, &len);
    if (state) {
        ErrorHandler("getsockopt error");
    }

    
    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&recvBuf, &len);
    if (state) {
        ErrorHandler("getsockopt error");
    }

    printf("input buffer size : %d, output buffer size : %d \n", recvBuf, sndBuf);

    return 0;
}

3.2 SO_REUSEADDR

   之前,我们遇到过服务端,服务端断开连接后同一端口无法立即使用,这是由于套接字主动关闭之后会进入time_wait状态。
  此状态有两个作用:①:允许老的重复报文分组在网络中消逝。②:保证TCP全双工连接的正确关闭。
  Time-wait看似重要,但不一定讨喜,因为如果系统发生故障而紧急重启,此时由于time-wait导致服务无法立即恢复,则引发了严重的问题。
  解决方案就是在套接字选项中更改SO_REUSEADDR的状态。适当调整该参数,可将time-wait状态下的套接字端口号重新分配给新的套接字。具体做法如下:

optlen = sizeof(option);
option = true;
setsockopt(servSock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, optlen);

3.3 TCP_NODEALY

Nagle算法

  在使用一些协议通讯的时候,比如Telnet,会有一个字节字节的发送的情景,每次发送一个字节的有用数据,就会产生41个字节长的分组,20个字节的IP Header 和 20个字节的TCP Header,这就导致了1个字节的有用信息要浪费掉40个字节的头部信息,这是一笔巨大的字节开销,而且这种Small packet在广域网上会增加拥塞的出现。
  如何解决这种问题? Nagle就提出了一种通过减少需要通过网络发送包的数量来提高TCP/IP传输的效率,这就是Nagle算法。
  Nagle算法主要是避免发送小的数据包,要求TCP连接上最多只能有一个未被确认的小分组,在该分组的确认到达之前不能发送其他的小分组。相反,TCP收集这些少量的小分组,并在确认到来时以一个分组的方式发出去。

禁用Nagle算法
  在默认的情况下,Nagle算法是默认开启的,Nagle算法比较适用于发送方发送大批量的小数据,并且接收方作出及时回应的场合,这样可以降低包的传输个数。同时协议也要求提供一个方法给上层来禁止掉Nagle算法

  当你的应用不是连续请求+应答的模型的时候,而是需要实时的单项的发送数据并及时获取响应,这种case就明显不太适合Nagle算法,明显有delay的。

  linux提供了TCP_NODELAY的选项来禁用Nagle算法。

//将套接字选项TCP_NODELAY改为1

int optVal = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optVal, sizeof(optVal));
  • 10
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值