TCP/IP网络编程 第六章:基于UDP的服务器端/客户端

UDP和TCP的区别

UDP(User Datagram Protocol)和TCP(Transmission Control Protocol)是两种常用的互联网传输协议,它们在数据传输方面有以下几个主要区别:

  1. 连接与无连接:TCP是面向连接的协议,而UDP是无连接的协议。TCP在进行数据传输前会先建立连接,然后进行数据传输,最后断开连接。而UDP在发送数据之前不需要建立连接。

  2. 可靠性:TCP提供可靠的数据传输,它使用确认机制和重传机制来确保数据的可靠性,能够检测丢包和重排数据包。UDP则没有这些机制,它不保证数据传输的可靠性,也无法检测丢包。

  3. 顺序性:TCP保证数据的顺序性,即发送的数据包按照顺序到达接收端。UDP发送的数据包可能会乱序到达接收端,接收端需要根据需要自行进行排序。

  4. 建立连接复杂性:TCP在建立连接时需要进行三次握手,比较复杂,但能够保证连接的可靠性。UDP不需要建立连接,因此建立连接的过程简单。

下面我结合具体情况来分析一下上述区别,对于这个第一点区别实际上就是TCP在连接之前要进行的三次握手和断开的四次挥手,当三握四挥完成后才真正代表一个TCP连接已经连接/断开。但UDP不需要连接,可以理解为它可以直接发送数据。对于第二点区别实际上是TCP的各种机制结合从而得到的一个特性,比如滑动窗口,超时重传,三握四挥,拥堵控制等等机制,而UDP缺少这些机制。第三和第四个区别实际上都是TCP拥有的丰富机制导致的,相比之下UDP没有这么丰富的机制。

但是这些繁杂的机制导致TCP的速度一般比UDP慢,虽然TCP在一些数据可靠性较高的地方发挥作用,UDP也在对实时性要求较高的直播,视频等领域发挥重要的作用。

实现基于UDP的服务器端/客户端

UDP中的服务器端和客户端没有连接

UDP服务器端/客户端不像TCP那样在连接状态下交换数据,因此与TCP不同,无需经过连接过程。也就是说,不必调用TCP连接过程中调用的listen函数和accept函数。UDP中只有创建套接字的过程和数据交换过程。

TCP中,套接字之间应该是一对一的关系。若要向10个客户端提供服务,则除了守门的服务器套接字外,还需要10个服务器端套接字。但在UDP中,不管是服务器端还是客户端都只需要1个套接字。可以把UDP的原理比作送信,收发信件时使用的邮筒可以比喻为UDP套接字。只要附近有1个邮筒,就可以通过它向任意地址寄出信件。同样,只需1个UDP套接字就可以向任意主机传输数据。

基于UDP的数据IO函数

由于TCP在发送数据之前已经将地址绑定在套接字中了,只需要往套接字里面“塞数据”就可以了。但UDP是无连接的,所以每一发送数据的时候都要确定一下发送目的地。就像送信一样,送信之前要确定一下送信的地址。接下来介绍一下UDP的数据传输函数。

#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   //可选项参数
      to      //存有目标地址的sockaddr结构体参数
      addrlen //传递给参数to的结构体长度

接下来是接受UDP数据的函数

#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   //可接受的最大字节数
        flags    //可选项参数
        from     //存有发送端地址的sockaddr结构体参数
        addrlen  //保存参数from结构体变量长度的地址值

编写UDP程序最核心的部分就在于上述的两个参数。

基于UDP的回声服务器端/客户端

下面结合之前的内容实现回声服务器。但是注意一下UDP和TCP不一样,因此在某种一样上无法明确区分服务器端和客户端。

以下是服务器端代码:

#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_sock;
    char message[BUF_SIZE];
    int str_len;
    socklen_t clnt_adr_sz;
    struct sockaddr_in serv_adr, clnt_adr;

    if(argcl=2){
        printf("Usage : %s <port>\n",argv[0]);
        exit(1);
    }

    serv_sock=socket(PF_INET, SOCK_DGRAM,0);
    if(serv_sock == -1)error_handling("UDP socket creation error");

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

    if(bind(serv_sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
           error_handling("bind() error");

    while(1){
       clnt_adr_sz=sizeof(clnt_adr);
       str_len=recvfrom(serv_sock, message, BUF_SIZE, 0,(struct sockaddr*)&clnt_adr, &clnt_adr_sz);
       sendto(serv_sock, message, str_len, 0,(struct sockaddr*)&clnt_adr, clnt_adr_sz);
    }
    close(serv_sock);
    return 0;
}

void error_handling(char *message){
     fputs(message, stderr);
     fputc('\n', stderr);
     exit(1);
}

下面介绍客户端的代码:

#include<"与服务端代码相同,故省略">
#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[]){
    int sock;
    char message[BUF_SIZE];
    int str_len;
    socklen_t adr_sz;
    struct sockaddr_in serv_adr, from_adr;

    if(argcl=3){
        printf("Usage :%s <IP> <port>\n", argv[e]);
        exit(1);
    }

    sock=socket(PF_INET, SOCK_DGRAM,0);
        if(sock==-1)error_handling("socket() error");

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

    while(1){
        fputs("Insert message(q to quit):",stdout);
        fgets(message, sizeof(message),stdin);
        if(!strcmp(message,"q\n")|| !strcmp(message,"e\n"))
           break;

        sendto(sock, message, strlen(message),0,(struct sockaddr*)&serv_adr, 
    sizeof(serv_adr));

    adr_sz=sizeof(from_adr);
    str_len=recvfrom(sock,message, BUF_SIZE, 0, (struct sockaddr*)&from_ adr, &adr_sz);
    message[str_len]=0;
    printf("Message from server: %s",message);
    }
    close(sock);
    return 0;
}

void error_handling(char *message){
    fputs(message, stderr);
    fputc('\n',stderr);
    exit(1);
}

UDP客户端套接字的地址分配

在上述的客户端代码中,我们发现客户端的套接字并没有有分配IP地址和端口号的过程,既然如此,UDP客户端何时分配IP地址和端口号。服务器端通过bind函数来分配IP地址和套接字,从而在sendto函数调用之前就分配好了。那么客户端在何时调用呢?这里就直接给出结论了,和TCP套接字的connect自动分配一样,UDP客户端IP地址和端口号分配是在调用sendto函数自动分配的。而且此时分配的地址一直保留到程序结束位置。

综上所述,调用sendto函数时自动分配IP和端口号,因此,UDP客户端中通常无需额外的地址分配过程。

已连接UDP套接字与未连接UDP套接字

UDP在通过sendto函数传输数据时会经过以下三个阶段:

1.向UDP套接字注册目标IP和端口号

2.传输数据

3.删除在UDP套接字中注册的目标地址信息

每次调用sendto函数都会重复上述三个过程的UDP套接字叫做未连接套接字。每次都变更目标地址,因此可以重复利用同一UDP套接字向不同的目标传递数据。但是如果此时想要向一个特定的目标地址长时间传递数据,那么此时过程1和过程2将会占用大量时间,如果可以缩短这部分时间的话,可以大大提高整体性能。

如果想要称为连接套接字的话,只要在前面的基础上调用这个函数即可

connect(sock,(struct sockaddr*)&addr,sizeof(addr));

这里要注意一下,这个所谓调用的connect函数并非想要与服务器套接字连接,它只是将地址信息注册进UDP套接字而已。

之后就和TCP套接字一样,每次调用sendto函数只需传输数据。进一步的,不仅可以使用sendto,recvfrom函数,还可以使用write,read函数进行通信。

基于Windows的实现

这里介绍一下Windows平台的sendto和recvfrom函数。实际上与Linux的函数没有太大区别。

#include<winsock2.h>
int sendto(SOCKET s,const char* buf,int len,int flags,const struct sockaddr*to,int tolen);
//成功时返回传输的字节数,失败时返回SOCKET_ERROR

int recvfrom(SOCKET s,char*buf,int len,int flag,struct sockaddr*from,int* fromlen);
//成功时返回接受的字节数,失败时返回SOCKET_ERROR

猜你喜欢

转载自blog.csdn.net/Reol99999/article/details/131702415