1 优雅地断开套接字连接

1.1 基于TCP的半关闭

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

单方面断开带来的问题 Linux的close函数和Windows的closesocket函数意味着完全断开连接,既不能传输数据,也不能接收。因此,一些情况下,某一方单独断开连接显得不太优雅。例如: 主机A和主机B进行通信,A向B发送完数据后,调用close断开连接,此时A将无法在发送和接收数据,那么B发送给A的数据也只能销毁了。 套接字和流 两台主机通过套接字建立连接后进行可交换数据状态,又称“流形成的状态”。即可把建立套接字后可交换数据的状态看作一种流。流是单方向的,所以一个套接字有两个流(输入和输出)。 可以看到主机的输入流与另一主机的输出流相连,主机的输出流与另一主机的输入流相连。 针对优雅断开的shutdown函数

#include

/**

* @param[2] : howto 传递断开方式信息

* 可选值如下:

* SHUT_RD :断开输入流,无法接收数据

* SHUT_WD :断开输出流,无法发送数据

* SHUT_RDWR:同时断开IO流

*/

int shutdown(int sock, int howto);

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

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

file_server.c

#include

#include

#include

#include

#include

#include

#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 \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

#include

#include

#include

#include

#include

#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 \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

/**

* @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

/**

*@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

#include

#include

#include

#include

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 \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

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

腾讯的DNS服务器IP示例

#include

#include

#include

#include

#include

#include

void ErrorHandler(char* message) {

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

int main(int argc, char**argv) {

if (argc != 2) {

printf("Usage: %s \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

/**

* @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

#include

#include

#include

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

#include

#include

#include

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));

参考链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: