1.UDP/IP协议栈
2.基于UDP/IP服务器端/客户端函数调用关系
PS: 客户端服务器不需要使用bind()
函数将地址信息分配到套接字上,这一操作在调用sendto()
函数时完成。
3.Linux系统实现迭代回声服务器/客户端
回声服务器/客户端的要求已经在前面的章节讲过了,这里不再赘述,下面是基于UDP的数据I/O函数
sendto()
函数
ssize_t sendto(
int sock, //这是套接字描述符,是一个整数值,唯一标识了一个打开的套接字。
const void *buff, //指向要发送的数据缓冲区的指针。
size_t nbytes, //这是要发送的数据的长度(以字节为单位)。
int flags,//这是一个选项标志,用于修改 sendto 函数的行为。
const struct sockaddr *to, //指向目标地址的指针,该地址结构包含了目的主机的网络地址信息。
socklen_t addrlen//这是目的地址结构 (to) 的长度。
);
recvfrom()
函数
ssize_t recvfrom(
int sock, //这是套接字描述符,是一个整数值,唯一标识了一个打开的套接字。
void *buff, // 指向一个缓冲区的指针,用于存储接收到的数据。
size_t nbytes, // 缓冲区的长度,即可以接收的最大数据量。
int flags,// 用于修改recvfrom行为的标志位,通常设置为0。
struct sockaddr *from, // 指向一个sockaddr结构的指针,用于存储发送方的地址信息。
socklen_t *addrlen// sockaddr结构的大小
);
服务器端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // POSIX标准定义的通用函数,如close()
#include <arpa/inet.h> // 提供inet相关的函数,如inet_addr()
#include <sys/socket.h> // 提供socket相关的函数和数据结构
#define BUFF_SIZE 30 //缓冲区大小
void error_handling(char* message);
int main(int argc, char *argv[])
{
int serv_sock; // 服务器套接字
int clnt_sock; // 客户端套接字
char opinfor[BUFF_SIZE];
int str_len,recvLen,clntAdrSize;
struct sockaddr_in serv_addr; // 服务器地址结构
struct sockaddr_in clnt_addr; // 客户端地址结构
socklen_t clnt_addr_size; // 客户端地址结构的大小
if(argc!=2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 创建一个服务器套接字
serv_sock=socket(PF_INET, SOCK_DGRAM, 0);//使用SOCK_DGRAM创建UDP套接字
if(serv_sock==-1)
error_handling("socket() error");
// 初始化服务器地址结构
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family=AF_INET; // 地址族设置为IPv4
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); // 服务器地址设置为任意
serv_addr.sin_port=htons(atoi(argv[1])); // 设置监听端口为命令行参数指定的端口
// 绑定套接字,调用bind()函数分配ip地址和端口号
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("bind() error");
while(1)
{
clnt_addr_size=sizeof(clnt_addr);
recvLen=recvfrom(serv_sock, opinfor, BUFF_SIZE, 0, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if(recvLen==-1)
error_handling("recvfrom() error");
sendto(serv_sock, opinfor, recvLen, 0, (struct sockaddr*)&clnt_addr, clnt_addr_size);
}
// 关闭客户端和服务器套接字
close(serv_sock);
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 BUFF_SIZE 30 // 缓冲区大小
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock; // 客户端套接字
struct sockaddr_in serv_addr; // 服务器地址结构
char opmsg[BUFF_SIZE]; // 用于接收从服务器发送的消息
int str_len;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
// 创建一个客户端套接字
sock = socket(PF_INET, SOCK_DGRAM, 0);//使用SOCK_DGRAM创建UDP套接字
if (sock == -1)
error_handling("socket() error");
// 初始化服务器地址结构
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET; // 地址族设置为IPv4
serv_addr.sin_addr.s_addr = inet_addr(argv[1]); // 设置服务器IP地址
serv_addr.sin_port = htons(atoi(argv[2])); // 设置服务器端口号
while (1)
{
fputs("Insert message(Q to quit): ", stdout);
fgets(opmsg, BUFF_SIZE, stdin);
if (!strcmp(opmsg, "q\n") || !strcmp(opmsg, "Q\n"))
break;
sendto(sock, opmsg, strlen(opmsg), 0, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
str_len = recvfrom(sock, opmsg, BUFF_SIZE, 0, NULL, 0);
opmsg[str_len] = 0;
printf("Message from server: %s", opmsg);
}
// 关闭套接字
close(sock);
return 0;
}
void error_handling(char* message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行代码
4.Windows系统实现迭代回声服务器/客户端
该程序示例迁移到Windows系统差异不大
sendto()
函数
int sendto(
SOCKET s,// 套接字描述符,用于指定发送数据的套接字。
const char FAR *buf,// 指向要发送的数据缓冲区的指针。
int len,// 要发送的数据长度。
int flags,// 控制选项,通常设置为 0。
const struct sockaddr FAR *to,// 指向目标地址的结构体指针,包含目的地址信息。
int tolen// 上述目标地址结构体的大小。
);
recvfrom()
函数
int recvfrom(
SOCKET s,// 上述目标地址结构体的大小。
char FAR *buf,// 指向用于存储接收数据的缓冲区的指针。
int len,// 缓冲区的长度。
int flags,// 控制选项,通常设置为 0。
struct sockaddr FAR *from,// 指向存储发送方地址的结构体指针。
int FAR *fromlen // 指向变量的指针,该变量在调用前包含 from 缓冲区的长度,在调用后包含返回地址结构的实际长度。
);
服务器端代码
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")// 指定链接到winsock库
#define BUFF_SIZE 30 //缓冲区大小
void error_handling(char* message);
int main(int argc, const char* argv[])
{
WSADATA wsaData;// Windows Sockets API需要的数据结构
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAddr, clntAddr;
char message[BUFF_SIZE];
int strlen,clntAdrSize;
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)// 初始化Windows Sockets服务
error_handling("WSAStartup() error!");
//创建套接字
hServSock = socket(PF_INET, SOCK_DGRAM, 0);//使用SOCK_DGRAM,即创建UDP套接字
if (hServSock == INVALID_SOCKET)
error_handling("socket() error");
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]));
// 将socket绑定到地址和端口
if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
error_handling("bind() error");
while(1){
clntAdrSize = sizeof(clntAddr);// 设置客户端地址结构的大小
strlen = recvfrom(hServSock, message, BUFF_SIZE, 0, (SOCKADDR*)&clntAddr, &clntAdrSize);
sendto(hServSock, message, strlen, 0, (SOCKADDR*)&clntAddr, clntAdrSize);
}
closesocket(hServSock);// 关闭服务器socket
WSACleanup();// 清理Windows Sockets服务
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 <winsock2.h>
#pragma comment(lib, "ws2_32.lib") // 指定链接到winsock库
#define BUFF_SIZE 30 // 缓冲区大小
void ErrorHandling(const char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr; // 用于存储服务器的地址信息
char message[BUFF_SIZE]; // 用于接收从服务器发送的消息
int message_size;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
// 初始化Windows Sockets服务
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");
// 创建套接字
hSocket = socket(PF_INET, SOCK_DGRAM, 0);//使用SOCK_DGRAM,即创建UDP套接字
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET; // 设置地址族为IPv4
servAddr.sin_addr.s_addr = inet_addr(argv[1]); // 设置服务器的IP地址
servAddr.sin_port = htons(atoi(argv[2])); // 设置服务器的端口号
while(1){
printf("Input message(Q to quit): ");
fgets(message, BUFF_SIZE, stdin);
if (!strcmp(message, "Q\n") || !strcmp(message, "q\n"))
break;
sendto(hSocket, message, strlen(message), 0, (SOCKADDR *)&servAddr, sizeof(servAddr));
message_size = recvfrom(hSocket, message, BUFF_SIZE, 0, NULL, NULL);
message[message_size] = 0;
printf("Message from server: %s", message);
}
closesocket(hSocket); // 关闭套接字
WSACleanup(); // 清理Windows Sockets服务
return 0;
}
void ErrorHandling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行代码
5.UDP的数据传输特性
- 无连接: UDP可以不建立连接发送数据
- 低延迟: 因为缺少连接建立的维护和开销,通常比TCP更快,但每次交换的数据量越大,TCP的速率越接近UDP
- 不保证完整性、不保证交付: 不能够保证数据报的顺序、完整性,也无法保证数据能够到达目的地
- 没有流控制: 流控制是UDP和TCP的重要区分标志。
- 由于以上特点,适合容忍数据丢失一部分的场景,例如视频流、游戏等
6.已连接的UDP和未连接的UDP
通过sendto()
函数传输数据大致分为三个阶段:
- 向UDP套接字注册目标IP和端口号
- 传输数据
- 删除UDP套接字注册目标IP和端口号
所以如果要和同一主机进行长时间的通信,使用已连接的UDP套接字可以提升效率,这样可以省去第一和第三阶段。
创建已连接的UDP套接字,使用connect()
函数,当使用已连接的UDP套接字之后,便可以使用write()/read()
函数进行通信
PS: 调用connect并不意味着和对方UDP套接字连接,只是向UDP套接字注册IP和端口号信息