6. 基于UDP的服务器端/客户端
6.1 理解UDP
TCP和UDP最重要的区别在于 流控制
这里的流控制应该包含了TCP的可靠传输、流量控制、拥塞控制等机制,这些机制都是在流上实现的
TCP更可靠,UDP更高效(TCP速度一般低于UDP,当每次传输数据很大时,两者速率会接近一点)
TCP在传输数据之前要建立连接(三次握手),而UDP不用。后者不询问接收方,直接发送数据
IP层将数据包传给主机B,而UDP就是将自己主机收到的数据包交给正确的套接字
TCP慢于UDP的原因:
(1)收发数据前后进行的连接设置及清除过程
(2)收发数据过程中为保证可靠性而添加的流控制
因此,当收发的数据量小但需要频繁连接时,UDP比TCP高效
深入学习:TCP/IP协议的内部构造
6.2 基于UDP的服务器端和客户端
UDP无需建立连接,因此不需要TCP中的listen和accept这两个步骤
TCP是一对一的,有多少客户端套接字,服务器端就需要创建多少个对应的套接字
UDP中,服务器端和客户端都仅需要1个套接字,就可以应对所有的数据传输请求(相当于每个家只需要有一个邮筒)
6.2.1 I/O函数
在TCP中,使用read和write时,只有3个参数,不需要指定对方套接字地址。因为之前在建立连接中就已经沟通过双方地址了
在UDP中,使用的是sendto和recvfrom函数,各6个参数。因此没有连接,所以在收发时要指明对方地址
- sendto()
#include <sys/socket.h>
ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
//成功时返回传输的字节数,失败时返回-1
sock 用于传输数据的UDP套接字文件描述符。
buff 保存待传输数据的缓冲地址值。
nbytes 待传输的数据长度,以字节为单位。
flags 可选项参数,若没有则传递0。
to 存有目标地址信息的sockaddr结构体变量的地址值。
addrlen 传递给参数to的地址值结构体变量长度。
- recvfrom()
#include <sys/socket.h>
ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
//成功时返回传输的字节数,失败时返回-1
sock 用于接收数据的UDP套接字文件描述符。
buff 保存接收数据的缓冲地址值。
nbytes 可接收的最大字节数,故无法超过参数buff所指的缓冲大小。
flags 可选项参数,若没有则传递0。
from 存有发送端地址信息的sockaddr结构体变量的地址值。
addrlen 保存参数from的结构体变量长度的变量地址值。
6.2.2 echo服务器端/客户端
UDP没有连接,从某种角度来说无法明确区分服务器端和客户端。这里将提供服务的一方成为服务器端
uecho_server.c
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#define BUF_SIZE 1024
void error_handling(const char* message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
int clnt_addr_size;
int i, str_len;
char message[BUF_SIZE];
if(argc != 2)
{
error_handling("wrong argc");
exit(1);
}
//1. socket
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(serv_sock == -1)
error_handling("socket() error!");
//2. bind
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("bind() error!");
//3. recvfrom和sendto直接收发数据
while(1)
{
clnt_addr_size = sizeof(clnt_addr);
str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
printf("receive: %s", message);
sendto(serv_sock, message, str_len, 0, (struct sockaddr *)&clnt_addr, clnt_addr_size);
}
close(serv_sock);
return 0;
}
uecho_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr, from_addr;
char message[BUF_SIZE];
int from_addr_size;
int str_len;
if(argc != 3)
{
error_handling("wrong argc");
exit(1);
}
//1. socket
sock = socket(PF_INET, SOCK_DGRAM, 0);
if(sock == -1)
error_handling("socket() error!");
//2. 设置服务器端的地址,但不再需要connect
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
//3. sendto和recvfrom收发数据,和服务器端一样
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
//strcmp相等返回0
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
sendto(sock, message, strlen(message), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
from_addr_size = sizeof(from_addr);
str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr *)&from_addr, &from_addr_size);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
在客户端代码中:
str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr *)&from_addr, &from_addr_size);
这一句也可以将from_addr修改为serv_addr。用另一个from_addr是为了避免收到其他方的信息,而将serv_addr覆盖掉
服务器端没有这个问题
UDP客户端套接字地址分配
在UDP中,服务器端和客户端没有那么明显的区别
sendto函数在调用的时候,如果发现之前没有使用bind函数,那么将自动分配IP地址和随机的端口号
一般服务器端会使用bind,客户端不使用
6.3 UDP的数据边界
TCP中不存在数据边界,但UDP是具有数据边界的协议
也就是说一方多少次sendto,另一方就应该多少次recvfrom
测试
UDP服务器端
for(i=0; i<3; ++i){
sleep(5);//延迟5秒接收,等待客户端3次发送完毕
clnt_addr_size = sizeof(clnt_addr);
str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
printf("Message %d: %s\n", i+1, message);
}
UDP客户端
char msg1[] = "Hi";
char msg2[] = "This is udp client";
char msg3[] = "Nice to meet you";
sendto(sock, msg1, strlen(msg1), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
sendto(sock, msg2, strlen(msg2), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
sendto(sock, msg3, strlen(msg3), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
UDP结果
每隔5秒,读入一次,和客户端的发送结果一样
TCP结果
用一样含义的代码,使用TCP时,会发现服务器端一次性读完了3次传来的数据。这就是没有数据边界的限制
6.4 UDP使用connect
sendto传输数据有3个阶段
(1)向UDP套接字注册目标IP和端口号
(2)传输数据
(3)删除UDP套接字中注册的目标地址信息
其中第1和第3个步骤占整个通信过程的约1/3
UDP默认无连接,但对于要多次sendto,这样的方式影响性能
可以创建连接的套接字,只需要对UDP套接字使用 connect
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//如果对UDP使用了connect,那么不仅可以使用sendto和recvfrom,还可以使用write和read
write(sock, message, strlen(message));
同样的,客户端3次发送,服务器端3次接收
注意:即便使用了connect,UDP也不会进行三次握手
6.5 windows平台
使用的是sendto和readfrom,其他类似
#include <winsock2.h>
int sendto(SOCKET s, const char* buf, int len, int flags, const struct sockaddr *to, int tolen);
//成功时返回传输的字节数,失败时返回SOCKET_ERROR
#include <winsock2.h>
int recvfrom(SOCKET s, char* buf, int len, int flag, struct sockaddr *from,int *fromlen) ;
//成功时返回接收的字节数,失败时返回SOCKET_ERROR
注意:
recvfrom接收的大小一点要大于等于sendto/send发送的大小
关闭服务器端后,客户端按理不能再收发数据。但一个有意思的现象是,在windows中,正在连接的服务器端关闭后,客户端会找相同地址的服务器端继续连接
例如uecho_server_win断开后,这里连接到了echo_server_win(可能是之前的echo_server_win没能关掉?)
uecho_server_win.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET serv_sock;
SOCKADDR_IN serv_addr, clnt_addr;
char buf[BUF_SIZE];
int recv_len, clnt_addr_size;
if(argc != 2)
error_handling("wrong argc");
//WSAStartup
if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
error_handling("WSAStartup() error");
//socket
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(serv_sock == INVALID_SOCKET)
error_handling("socket() error");
//bind
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[1]));
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(serv_sock, (SOCKADDR *)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
error_handling("bind() error");
//收发数据
while(1)
{
clnt_addr_size = sizeof(clnt_addr);
//接收
recv_len = recvfrom(serv_sock, buf, BUF_SIZE-1, 0, (SOCKADDR *)&clnt_addr, &clnt_addr_size);
if(recv_len == SOCKET_ERROR)
error_handling("recvfrom() error");
buf[--recv_len] = 0;//删除末尾的换行符(客户端输入时的那个回车符会被传过来),增加结尾符
printf("received from client: %s\n", buf);
//发送
strcat(buf, "_return");
sendto(serv_sock, buf, recv_len+7, 0, (SOCKADDR *)&clnt_addr, sizeof(clnt_addr));
}
closesocket(serv_sock);
WSACleanup();
return 0;
}
uecho_client_win.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 1024
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET clnt_sock;
SOCKADDR_IN serv_addr, clnt_addr;
char buf[BUF_SIZE];
int recv_len;
if(argc != 3)
error_handling("wrong argc");
//WSAStartup
if(WSAStartup(MAKEWORD(2, 2), &wsaData) == -1)
error_handling("WSAStartup() error");
//socket
clnt_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(clnt_sock == INVALID_SOCKET)
error_handling("socket() error");
//connect
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(atoi(argv[2]));
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(clnt_sock, (SOCKADDR *)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
error_handling("connect() error");
//收发数据
int cln_size = 0;
while(1)
{
fputs("Input message (Q to quit): ", stdout);
fgets(buf, BUF_SIZE, stdin);
if(!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
break;
send(clnt_sock, buf, strlen(buf), 0);
recv_len = recv(clnt_sock, buf, BUF_SIZE-1, 0);
buf[recv_len] = 0;
printf("Message from server: %s\n", buf);
}
closesocket(clnt_sock);
WSACleanup();
return 0;
}
7. 优雅地断开套接字连接
7.1 基于TCP的半关闭
1. 单方面断开连接带来的问题
场景:主机A B正在通信。主机A发送完最后的数据后就调用close()断开了连接。此时主机A无法再接收数据,于是在收到“断开”信息之前,这期间主机B传过去的数据也就销毁了。
解决方法:Half-close,连接的半关闭。指可以传输,但无法接收。或者可以接收,但无法传输
2. 套接字和流(Stream)
A B两个主机建立连接之后,各拥有2个流,分别是输入流和输出流。A的输入流和B的输出流对接,A的输出流和B的输入流对接
Half-close 就是指断开其中的一个流连接
close()和closesocket()都是同时断开2个流
shutdwon() 函数
用于半关闭的函数shutdown,关闭其中一个流
int <sys/socket.h>
int shutdown(int sock, int howto);
//成功时返回0,失败时返回-1
sock 需要断开的套接字文件描述符
howto 传递断开方式信息
半关闭的理解
场景:服务器端向客户端发送一个文件,客户端收到之后返回给服务器端 “thank you”
问题1:服务器端只需要不断发送数据直至完毕,但客户端不知道接收完毕,一直 read(),等得不到数据将陷入阻塞状态
解决:约定一个文件结束符(这个结束符不能和出现在文件内容,因此只能通过单独传输一次结束符来避免)
问题2:服务器端发送了 EOF 文件结束符后,调用close()关闭连接。客户端返回的 “thank you” 无法被接收
解决:客户端在发送EOF后,只关闭输出流,保留输入流
基于半关闭的文件传输程序
file_server.c
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#define BUF_SIZE 1024
int main(int argc, char* argv[]){
int serv_sock, clnt_sock;
FILE *fp;
struct sockaddr_in serv_addr, clnt_addr;
int clnt_addr_size;
int i, read_cnt;
char buf[BUF_SIZE];
if(argc != 2)
{
printf("wrong argc\n");
exit(1);
}
//打开文件
fp = fopen("file_server.c", "rb");//r表示只读,b表示二进制(文件必须已存在)
//socket
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
//2. bind
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//listen
listen(serv_sock, 5);
//accept
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
//读文件,发送数据
while(1)
{
read_cnt = fread((void *)buf, 1, BUF_SIZE, fp);
printf("send message %d\n", read_cnt);
if(read_cnt<BUF_SIZE)
{
write(clnt_sock, buf, read_cnt);
break;
}
write(clnt_sock, buf, BUF_SIZE);
}
//半关闭socket
shutdown(clnt_sock, SHUT_WR);//关闭输出流,保留输入流
read(clnt_sock, buf, BUF_SIZE);
printf("Message from client: %s\n", buf);
//关闭socket
fclose(fp);
close(clnt_sock);
close(serv_sock);
return 0;
}
file_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
int main(int argc, char* argv[])
{
int clnt_sock;
FILE *fp;
struct sockaddr_in serv_addr, from_addr;
char buf[BUF_SIZE];
int from_addr_size;
int read_cnt;
if(argc != 3)
{
printf("wrong argc");
exit(1);
}
//打开文件
fp = fopen("receive.dat", "wb");//不存在则创建,存在则覆盖
//socket
clnt_sock = socket(PF_INET, SOCK_STREAM, 0);
//connect
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
connect(clnt_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
//接收文件,并返回 thank you
while((read_cnt = read(clnt_sock, buf, BUF_SIZE)) != 0)
fwrite((void *)buf, 1, read_cnt, fp);
puts("Received file data");
write(clnt_sock, "Thank you", 10);
//关闭
fclose(fp);
close(clnt_sock);
return 0;
}
结果和说明
上述程序能够完成之前的场景要求
shutdown(clnt_sock, SHUT_WR);//关闭输出流,保留输入流
shutdown和close的区别:
- shutdown总能够发送FIN报文,但close不一定
- close存在计数,并不一定导致该套接字不可用,shutdown不管引用计数,直接使得套接字失效,如果其他进程使用该套接字,将会受到影响。
- close会关闭连接,并释放所有连接对应的资源,而shutdown并不会释放套接字和所有资源
7.2 windows下的Half_close
#include <winsock2.h>
int shutdown(SOCKET sock, int howto);
//成功时返回0,失败时返回SOCKET_ERROR
注意第二个参数的值写法有所不同
分别以0,1,2表示,和linux下的是一样的,只是名字不同
实现:略
8. 域名及网络地址
DNS,Domain Name System,域名系统,用于IP地址和域名的相互转换,核心是DNS服务器
百度的IP地址:39.156.66.18(不唯一)
百度的域名:www.baidu.com
输入以上的任意一个都可以打开百度。但实际上,输入域名时,需要通过DNS服务器将域名转换为IP地址
所有计算机中都记录着默认DNS服务器地址,就是通过这个默认DNS服务器得到相应域名的IP地址信息
一般不会更改域名,但会更改IP地址。编写程序时应当多使用域名而非IP地址
查看域名对应的IP地址:ping www.baidu.com
查看默认DNS服务器地址:nslookup (linux中还要根据提示信息输入 server)
默认DNS如果找不到该域名,则逐级向上询问。最后根DNS会知道向哪个DNS服务器请求解析域名。最后解析得到的IP地址原路返回
DNS是一种分布式数据库系统
当默认DNS找到该域名时:
DNS是将域名转为IP地址,路由器根据IP地址选择路径。DNS和路由器是不同的概念
DNS和操作系统无关
8.1 利用域名获取IP地址 hostent
根据字符串格式的域名获取到ip地址
#include <netdb.h>
struct hostent * gethostbyname(const char *hostname);
//成功时返回hostent结构体地址,失败时返回NULL指针
hostent结构体
struct hostent
{
char *h_name; //official name
char **h_aliases; //alias list
int h_addrtype; //host address type
int h_length; //address length
char **h_addr_list; //address list
};
//重点是h_addr_list
h_name 官方域名,但有些公司并未用官方域名注册
h_aliases 可以通过多个城名访问同一主页。同一IP可以绑定多个域名,因此,除官方域名外还可指定其他域名
h_addrtype 获取保存在h_addr_list中的IP地址的地址族信息,如果是IPv4,则保存的是AF_INET
h_length IP地址长度,如果是IPv4,就是4;如果是IPv6,就是16
h_addr_list 通过此变量以整数形式保存域名对应的IP地址
其中,h_addr_list其实是一个指针数组,数组中每个元素都是in_addr型指针
没有写成 in_addr** 是为了提高通用性,因为char *和in_addr *都是4字节的指针
现在一般用void *来处理这种情况,但当时定义套接字时是在void指针标准化之前,那时使用char *指代不明确类型
测试
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netdb.h>
int main(void)
{
int i;
char domain[30] = "www.baidu.com";
struct hostent *host;
host = gethostbyname(domain);
if(!host)
{
printf("gethostbyname() error\n");
exit(1);
}
//查看IP地址信息
printf("Official name: %s\n", host->h_name);
for(i=0; host->h_aliases[i]; ++i)
printf("Aliases %d: %s\n", i+1, host->h_aliases[i]);
printf("Address type: %s\n", (host->h_addrtype == AF_INET)?"AF_INET":"AF_INET6");
printf("Address length: %d\n", host->h_length);
for(i=0; host->h_addr_list[i]; i++)
printf("IP addr %d: %s\n", i+1, inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
return 0;
}
8.2 利用IP地址获取域名
#include <netdb.h>
struct hostent * gethostbyaddr(const char *addr, socklen_t len, int family);
//成功时返回hostent结构体地址,失败时返回NULL指针
addr 含有IP地址信息的in_addr结构体指针。为了同时传递IPv4地址之外的其他信息,该变量的类型声明为char指针
len 向第一个参数传递的地址信息的字节数, IPv4时为4,IPv6时为16
family 传递地址族信息,IPv4时为AF_INET,IPv6时为AF_INET6
测试
问题:
使用gethostbyaddr时,使用网上查到的百度ip地址202.108.22.5可以成功解析
但是使用gethostbyname得到的ip地址39.156.66.18返回的是NULL,尽管这一ip能ping通(原因?)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
int main(void)
{
int i;
//char ip[30] = "39.156.66.18";
//char ip[30] = "74.125.19.106";
char ip[30] = "202.108.22.5";
struct hostent *host;
struct in_addr addr;
addr.s_addr = inet_addr(ip);
host = gethostbyaddr((char *)&addr, 4, AF_INET);
if(!host)
{
printf("gethostbyname() error\n");
perror("gethostbyaddr");
exit(1);
}
printf("Official name: %s\n", host->h_name);
for(i=0; host->h_aliases[i]; ++i)
printf("Aliases %d: %s\n", i+1, host->h_aliases[i]);
printf("Address type: %s\n", (host->h_addrtype == AF_INET)?"AF_INET":"AF_INET6");
printf("Address length: %d\n", host->h_length);
for(i=0; host->h_addr_list[i]; i++)
printf("IP addr %d: %s\n", i+1, inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
return 0;
}
8.3 windows平台
有差不多的函数
#include <winsock2.h>
struct hostent * gethostbyname(const char * name);
//成功时返回hostent结构体变量地址值,失败时返回NULL指针。
struct hostent * gethostbyaddr(const char *addr,int len, int type);
//成功时返回hostent结构体变量地址值,失败时返回NULL指针。
在windows平台下,代码上除了要修改头文件和添上WSA语句,其他一样;
结果也一样
9. 套接字的多种可选项
SOL_SOCKET 套接字相关的通用可选项
IPPROTO_IP IP协议相关事项
IPPROTO_TCP TCP协议相关事项
9.1 getsockopt 和 setsockopt 和 SNDBUF 和 RCVBUF
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
//成功时返回0,失败时返回-1
level 协议层
optname 要查看的可选项名
optval 保存查看结果的缓冲地址值
optlen 保存optval传递的缓冲大小
#inlclude <sys/socket.h>
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
//成功时返回0,失败时返回-1
示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
int main(void){
int tcp_sock, udp_sock;
int sock_type;
socklen_t optlen;
int state;
optlen = sizeof(sock_type);
tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
udp_sock = socket(PF_INET, SOCK_DGRAM, 0);
printf("SOCK_STREAM: %d \n", SOCK_STREAM);
printf("SOCK_DGRAM: %d \n", SOCK_DGRAM);
//获取可选项
state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);
if(state){
printf("getsockopt() error!");
exit(1);
}
printf("Socket type one: %d \n", sock_type);
state = getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);
if(state){
printf("getsockopt() error!");
exit(1);
}
printf("Socket type two: %d \n", sock_type);
return 0;
}
SO_TYPE是只读选项
“套接字类型只能在创建时决定, 以后不能再更改。”
获取输入缓冲大小和输出缓冲大小
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
int main(void){
int sock;
int snd_buf, rcv_buf;
socklen_t buflen;
int state;
//建立套接字
sock = socket(PF_INET, SOCK_STREAM, 0);
//获取输出缓冲
buflen = sizeof(snd_buf);
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &buflen);
if(state){
printf("getsockopt() error!");
exit(1);
}
//获取输入缓冲
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &buflen);
if(state){
printf("getsockopt() error!");
exit(1);
}
printf("Input buffer size: %d \n" , rcv_buf);
printf("Output buffer size: %d \n" , snd_buf);
return 0;
}
setsockopt示例
//设置输入缓冲和输出缓冲
int snd_buf=1024*3, rcv_buf=1024*3;
state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, sizeof(snd_buf));
state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, sizeof(rcv_buf));
9.2 SO_REUSEADDR
time-wait
主机A率先发送断开连接请求时,在四次握手过程中,最后一个消息由主机A发送。如果这最后一个消息丢失,那么主机B没有收到确认,就会认为自己的最后一次发送(第三次握手)对方没有收到,于是发起重传。但是因为A关闭后无法收到B的重传,因此B收不到确认就会一直重传。基于这些考虑,先传输Fin消息的主机应经过Time-wait过程客户端不考虑time-wait是因为客户端的端口一般是随机分配的
如果出现延迟、丢失,time-wait计时器不断重启,将导致服务器迟迟无法重启
time-wait存在的意义
1.保证第四次握手客户端发送的最后一个ACK报文段 能够到达服务端,可靠地实现了TCP全双工连接的终止;
2.防止“已失效的连接请求报文段”出现在本连接中
例如回声代码中,如果服务器端断开连接,那么不能立即以同样的端口号重启服务器,会出现bind() error(试了一下,发现并无影响,仍待观察)。取消time-wait只需要设置如下代码
int option;
optlen=sizeof(option);
option=TRUE;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&option, optlen);
SO_REUSEADDR 的默认值为 0,将其修改为 1 即可将 time-wait 状态下的套接字端口号重新分配给新的套接字
9.3 TCP_NODELAY 和 Nagle算法
Nagle算法诞生于1984年,应用于TCP层,是为了防止数据包过多而发生的网络过载
Nagle算法:只有收到前一数据的ACK消息,才发送下一数据
TCP套接字默认使用Nagle算法交换数据
以传输“Nagle”字符串为例(极端情况):
Nagle on:N率先被发送,等待N的ACK时,agle填入输出缓冲;拿到ACK后,将agle数据包发送。一共4个数据包
Nagle off:5个字符和5个ACK, 传输10个数据包
禁用Nagle算法可以提高传输速度,但增加了网络流量
禁用Nagle算法的场景:最典型的是“传输大文件数据”,数据传入输出缓冲不会花太多时间,这种情况即便不使用Nagle每次传输的数据包也是满的,因而不用Nagle会更好
//禁用Nagle
int optval = 1;
state = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, sizeof(opt_val));
同样可以用getsockopt查看Nagle使用与否
正在使用为0,已禁用为1
示例:
#include <stdio.h>
#include <stdlib.h>
//#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/tcp.h>
int main(void){
int sock;
socklen_t len;
int state;
//建立套接字
sock = socket(PF_INET, SOCK_STREAM, 0);
//获取默认的值
int optval = -1;
len = sizeof(optval);
state = getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optval, &len);
printf("Default TCP_NODELAY value: %d \n" , optval);
//修改,禁用Nagle算法
optval = 1;
state = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optval, len);
state = getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optval, &len);
printf("Modified value: %d \n" , optval);
return 0;
}
9.4 windows下的实现
套接字可选项及其相关内容和操纵系统无关,在linux下和windows下是一样的,如:
#include <winsock2.h>
int setsockopt(SOCKET sock, int level, int optname, const char* optval, int optlen);
//成功时返回0,失败时返回SOCKET_ERROR。
注意的是,这里optval从linux下的 void* 变为了 char*