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);
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);
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);
}
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(serv_sock == -1)
error_handling("socket() error!");
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!");
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);
}
sock = socket(PF_INET, SOCK_DGRAM, 0);
if(sock == -1)
error_handling("socket() error!");
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]));
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
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);
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));
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);
#include <winsock2.h>
int recvfrom(SOCKET s, char* buf, int len, int flag, struct sockaddr *from,int *fromlen) ;
注意:
-
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");
if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
error_handling("WSAStartup() error");
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(serv_sock == INVALID_SOCKET)
error_handling("socket() error");
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");
if(WSAStartup(MAKEWORD(2, 2), &wsaData) == -1)
error_handling("WSAStartup() error");
clnt_sock = socket(PF_INET, SOCK_DGRAM, 0);
if(clnt_sock == INVALID_SOCKET)
error_handling("socket() error");
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);
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");
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
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(serv_sock, 5);
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);
}
shutdown(clnt_sock, SHUT_WR);
read(clnt_sock, buf, BUF_SIZE);
printf("Message from client: %s\n", buf);
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");
clnt_sock = socket(PF_INET, SOCK_STREAM, 0);
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));
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);
那么客户端将陷入阻塞,直到服务器端调用close()函数,才会read()完毕。但这样就导致服务器端无法接收到发来的 ”Thank you“
7.2 windows下的Half_close
#include <winsock2.h>
int shutdown(SOCKET sock, int howto);
注意第二个参数的值写法有所不同
分别以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结构体
struct hostent
{
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **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);
}
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;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9NDkZN5g-1665554664082)(C:\Users\jiang\AppData\Roaming\Typora\typora-user-images\image-20221004224859132.png)]
8.2 利用IP地址获取域名
#include <netdb.h>
struct hostent * gethostbyaddr(const char *addr, socklen_t len, int family);
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] = "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;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYW4kxfH-1665554664084)(C:\Users\jiang\AppData\Roaming\Typora\typora-user-images\image-20221004232629918.png)]
8.3 windows平台
有差不多的函数
#include <winsock2.h>
struct hostent * gethostbyname(const char * name);
struct hostent * gethostbyaddr(const char *addr,int len, int type);
在windows平台下,代码上除了要修改头文件和添上WSA语句,其他一样; 结果也一样
9. 套接字的多种可选项
|