目录
基于 UDP 的服务器端和客户端
实现基于 UDP 的服务器端可客户端
UDP 重要特性
- UDP 无需调用 listen 以及 accept 建立连接
- UDP 仅有创建套接字过程和数据交换过程
- 无论服务端还是客户端都只需要 1 个套接字
- UDP 是无状态的,故每次传输都必须加上目标地址
UDP 收发函数
#include <sys/socket.h>
// 参数:sock:UDP 套接字文件描述符; buff:用户保存接收的数据; nbytes:可接收的最大字节数;
// flags:可选项参数,没有则为 0; to:包含目标地址信息; addrlen:包含目标地址信息的结构体变量的长度
// 返回值:成功时返回接收的字节数,失败时返回 -1。
ssize_t sendto(int sock, void* buff, size_t nbytes, int flags, struct sockaddr* to, socklen_t addrlen);
#include <sys/socket.h>
// 参数:sock:UDP 套接字文件描述符; buff:待传输的数据; nbytes:待传输的数据长度(单位是字节);
// flags:可选项参数,没有则为 0; from:用来存储发送端的地址信息; addrlen:包含发送端地址信息的结构体变量的长度
// 返回值:成功时返回传输的字节数,失败时返回 -1。
ssize_t recvfrom(int sock, void* buff, size_t nbytes, int flags, struct sockaddr* from, socklen_t *addrlen);
UDP 如果在调用 sendto 函数式发现未分配地址信息,则首次调用 sendto 函数时会自动为套接字分配 IP 和端口
UDP 传输特性与 connect 函数
存在数据边界的 UDP 套接字
UDP 是存在数据边界的协议;
即强调输入函数的调用次数必须等于输出函数的调用次数!
只有这样才可以确保数据完整性
已连接与未连接套接字
通过 sendto 传输数据过程可分为:
- 向 UDP 套接字注册 IP 与端口
- 传输数据
- 删除 UDP 套接字中注册的目标地址信息
当需要长时间链接时,把 UDP 变成已连接套接字就会节省掉步骤 1、3 所消耗的时间
创建已经链接的 UDP 套接字
// adr为目标地址信息
connect(sock, (struct sockaddr*)&adr, sizeof(adr));
优雅的断开套接字链接
什么是半关闭状态?
比如两台主机双向通信,A 主机正在传输数据的瞬间立刻执行 close 函数关闭链接,则 B 主机立刻停止接收任何数据,故仍在路上传输的数据就会被丢弃!!!
半关闭指的是:可传输数据但无法接收、不可传输但是可接受,两种主要状态
使用 shutdown 函数断开
#include <sys/socket.h>
// shutdown函数只断开其中一个流
// sock:需要断开的套接字,howto:断开的方式
int shutdown(int sock, int howto);
第二个参数,断开的方式,可以选择以下三种的一个:
SHUT_RD
:断开输入流,此后套接字无法接收数据;SHUT_WR
:断开输出流,此后套接字无法发送数据;SHUT_RDWR
:同时断开 I/O 流。
基于半关闭状态的服务器和客户端代码
首先来看看服务器代码
#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 serv_sd, clnt_sd;
FILE *fp;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if (argc != 2)
{
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
fp = fopen("file_server.c", "rb");
serv_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 = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
bind(serv_sd, (struct sockaddr *)&serv_adr, sizeof(serv_adr));
listen(serv_sd, 5);
clnt_adr_sz = sizeof(clnt_adr);
clnt_sd = accept(serv_sd, (struct sockaddr *)&clnt_adr, &clnt_adr_sz); // 这里 accept 函数只调用了一次,此程序一次运行期间实际上只能接受一次连接。
while (1)
{
read_cnt = fread((void *)buf, 1, BUF_SIZE, fp);
if (read_cnt < BUF_SIZE)
{
write(clnt_sd, buf, read_cnt);
break;
}
write(clnt_sd, buf, BUF_SIZE);
}
shutdown(clnt_sd, SHUT_WR); // 关闭了输出流
read(clnt_sd, buf, BUF_SIZE); // 还可以继续接收数据
printf("Message from client: %s \n", buf);
fclose(fp);
close(clnt_sd);
close(serv_sd);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
再看看客户端的
#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)
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);
}
域名及网络地址
域名系统
DNS 域名系统,可以将 IP 和域名之间进行相互转换
当前级别 DNS 服务器无法识别当前域名或者 IP 时,会自动请求上一级 DNS 服务器
IP 与域名之间转换
利用域名获取 IP
#include <netdb.h>
// 返回包含IP地址详细信息的指针
struct hostent* gethostbyname(const char* hostname);
套接字的多种可选项
套接字可选项与 IO 缓冲大小
IO 可选项有以下三个主要层次:
IPPROTO_IP
:IP 协议相关事项;IPPROTO_TCP
:TCP 协议相关事项;SOL_SOCKET
:套接字相关的通用可选项。
读取和设置套接字可选项(但有些可选项是只读的,这里要注意!)
#include <sys/socket.h>
// sock 套接字
// level 可选项的协议层
// optname 可选项名
// optval 保存查看结果的缓冲地址值
// optlen 向第四个参数传递缓冲大小
int getsockopt(int sock, int level, int optname, void* optval, socklen_t* optlen);
// 对应参数和getsockopt一致
int setsockopt(int sock, int level, int optname, void* optval, socklen_t optlen);
SO_SNDBUF
可选项表示输出缓冲大小相关信息
SO_RCVBUF
可选项表示输入缓冲大小相关信息。
缓冲区大小可以被更改,但是仍然会有些许出入
SO_REUSEADDR
Time-wait 状态
TCP 协议中的一种状态,指的是 TCP 连接关闭后,等待一段时间以确保远程端口收到了 ACK 确认报文段后进入的一种状态
在 TCP 协议中,一旦连接关闭,就会生成一个 FIN 报文段,向远程主机发送关闭请求。此时,如果远程主机发送了 ACK 确认报文段,但是这个 ACK 丢失了,那么本地主机就会一直等待,直到超时时间(超时时间通常是 2 倍的最大报文段生存时间(Maximum Segment Lifetime,MSL),MSL 的典型值为 2 分钟)结束,才会离开 time-wait
状态
地址分配错误(binding error)
服务器端使用 ctrl+c 强行终止程序后,会陷入 time-wait 状态,若此时立即以同一端口号重新运行服务器端,即发生 Binding error
故只能等待几分钟后再重启服务器
客户端强行终止不会有任何影响
地址再分配
将 SO_REUSEADDR
设置为 1,表示允许重分配 time-wait 状态下的端口号
默认 SO_REUSEADDR
值为 0,则为不允许
TCP_NODELAY
Nagle 算法
Nagle
算法是一种改善 TCP 网络传输性能的算法,它的主要作用是减少网络传输的小分组数目,从而提高网络传输效率
Nagle
算法的基本实现思想:当发送端发送一个数据包后,如果当前缓存区中还有数据未被发送,那么就将这个数据包放入缓存区中,等待一段时间再发送。
等待时间是通过 TCP_NODELAY
选项来设置的,一般情况下默认为开启状态,即不等待,立即发送。
当需要启用 Nagle 算法时,需要将 TCP_NODELAY 选项设置为关闭状态。
优点:避免大流量
缺点:产生 N 多个小体积包,加大网络负载;降低传输速度,因为需要等待 ACK 报文,特别是传输大文件的时候;