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地址比域名发生变更的概率要高
- 利用域名获取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;
}
- 利用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的。文章来源:https://www.toymoban.com/news/detail-825977.html
linux
提供了TCP_NODELAY
的选项来禁用Nagle算法。文章来源地址https://www.toymoban.com/news/detail-825977.html
//将套接字选项TCP_NODELAY改为1
int optVal = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optVal, sizeof(optVal));
到了这里,关于五:优雅断连 & 域名<=>IP & 套接字多种选项的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!