TCP服务器端 & UDP服务器端 的实现差异【1】

一、回顾读写api以及基础api

数据读写api

TCP数据读写

对文件的读写操作read和 write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:

recv()

ssize_t recv(int sockfd,void *buf,size_t len,int flags);
recv读取sockfd 上的数据,buf和len参数分别指定读缓冲区的位置和大小,flags参数通常设置为0。
成功时返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能要多次调用recv,才能读取到完整的数据。recv返回0表示对方已经关闭连接了。recv出错时返回-1并设置errno。

send()

ssize_t send(int sockfd,const void *buf,size_t len,int flags);
send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。send成功时返回实际写入的数据的长度,失败则返回-1并设置errno。

UDP数据读写

另外,这两个函数也可以用于面向连接(STREAM)的socket数据读写,只需要把最后两个参数设置为NULL。

因为UDP通信没有连接的概念,所以我们每次读/写数据都需要 获取发送端(读)/指定接收端(写) 的socket地址和长度。

除此之外,返回值与其它参数的含义均与recv()send()相同。

recvfrom()

ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr* src_addr,socklen_t* addrlen);
recvfrom调用会获取数据传输端的地址,并填充到clnt_adr(因此serv端知道是从哪个客户端接收的数据),正是利用该地址将接收的数据逆向传输 。

读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小。因为UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数src_addr所指的内容,addrlen则指定该地址的长度

sendto()

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。dest_addr指定接收端的socket地址,addrlen指定该地址的长度

基础api accept()

accept函数用于从Accepte队列中pop出一个已完成的连接,若Accept队列为空,则accept函数所在的进程阻塞。

需要注意的是,accept函数从listen监听队列中(取出)接收一个连接,而不论该连接处于何种状态(即使客户端网络异常掉线),更不关心网络状况的变化

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);

  • p1: 监听socket(门卫)
  • p2: addr is a pointer to a sockaddr structure. This structure is filled in with the address of the peer socket (accept调用时会将其填充为客户端socket地址,大小由参数addrlen指出//传出参数
  • p3: addrlen is a value-result argument(传入传出参数): the caller must initialize it to contain the size of the structure pointed to by addr; on return it will contain the actual size of the peer address(传入的是sockaddr结构体的大小,传出的是客户端socket地址的实际大小。accept调用后就被填入客户端socket地址结构体中)
  • 返回值:成功返回创建的新的连接socket,失败返回-1并设置errno

我们把 执行过listen调用、处于LISTEN状态 的socket称为监听socket,而所有处于ESTABLISHED状态的socket则称为连接socket.

二、实现TCP回声服务器端/客户端

echo_serv.c

#include <stdio.h>      // Standard input/output functions
#include <stdlib.h>     // Standard library functions
#include <string.h>     // String handling functions
#include <unistd.h>     // Unix standard functions
#include <arpa/inet.h>  // IP address conversion functions
#include <sys/socket.h> // Socket functions
#define BUF_SIZE 1024   // Define a constant for the buffer size
void error_handling(char* message); // Function prototype for error handling

int main(int argc, char* argv[]) {
    //argv[0] is the program name,argv[1] is port number
    int  serv_sock, clnt_sock;
    //serv_sock: server socket(gatekeeper),
    //clnt_sock: connection socket to communicate with the client
    char message[BUF_SIZE];
    struct sockaddr_in serv_adr, clnt_adr; 
    socklen_t clnt_adr_sz; //initialize clnt_adr_sz
    
    if(argc != 2) { //Two parameters are required, one is the program name, and the other one is port number
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }   
    
    // Creates a (gatekeeper) socket.
    serv_sock = socket(PF_INET, SOCK_STREAM, 0); 
    if(serv_sock == -1) error_handling("socket() error");
    
    //Initialize the server address structure (gatekeeper)
    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之前设置socket选项SO_REUSEADDR,
    //表示允许地址重用, 否则close之后会有一个WAIT_TIME状态,使得该ip和端口仍>被占用,产生bind() error.
    int on = 1;
    if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int)) < 0)  error_handling("setsockopt() error");
    
    // Bind the (gatekeeper) socket, also listening socket later.
    if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error"); 
    
    //Listen for incoming connections,set backlog queue of size 5.
    if(listen(serv_sock, 5) == -1) error_handling("listen() error");
    
    int i, str_len;
    clnt_adr_sz = sizeof(clnt_adr); //initialize clnt_adr_sz
    // Accept up to 5 client connections
    for (i = 0; i < 5; ++i) {
        // 取出连接,并返回与对应客户端进行连接的连接socket
        clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_adr, &clnt_adr_sz); 
        if(clnt_sock == -1) error_handling("accept() error");
        else printf("the %dth connected client \n", i + 1);
        
        // Read data from the client and write it back
        while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0) 
        	write(clnt_sock, message, str_len);
        close(clnt_sock); //关闭与客户端通信的连接socket
    }
    close(serv_sock); // Close the server socket (gatekeeper)
    return 0;
}
void error_handling(char* message) {
    perror(message);  // 打印错误信息
    exit(1);  // 退出程序
}

echo_clnt.c

#include <stdio.h>      // Standard input/output functions
#include <stdlib.h>     // Standard library functions
#include <string.h>     // String handling functions
#include <unistd.h>     // Unix standard functions
#include <arpa/inet.h>  // IP address conversion functions
#include <sys/socket.h> // Socket functions
#define BUF_SIZE 1024   // Define a constant for the buffer size
void error_handling(char* message); // Function prototype for error handling

int main(int argc, char* argv[]) {
    //argv[0] is the program name,argv[1] and argv[2] are the IP and port for the target server socket (gatekeeper Socket)
    int clnt_sock;
    char message[BUF_SIZE]; //Buffer to hold messages
    struct sockaddr_in serv_adr; //Server address structure
    
    // Check if the correct number of command line arguments is provided
    if(argc != 3) { //Three parameters are required, one is the program name, and the other two are the IP and port number
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }   
    
    //Creates a TCP socket.
    clnt_sock = socket(PF_INET, SOCK_STREAM, 0); 
    if(clnt_sock == -1) error_handling("socket() error");
    
    //Initialize the server address structure
    memset(&serv_adr, 0, sizeof(serv_adr)); //Clears the structure for server address.   
    serv_adr.sin_family = AF_INET; //Sets the address family to IPv4
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]); //Sets the server IP address
    serv_adr.sin_port = htons(atoi(argv[2])); //Sets the server port.
    
    // Connects to the server,and connect() will also assign address info to client socket        
    if(connect(clnt_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) error_handling("connect() error");    
    else puts("Connected!");
    
    int str_len, recv_len, recv_cnt;
    // Main loop for communication:
    while (1) {
        fputs("Input message(Q to quit): ", stdout);//standard output
        fgets(message, BUF_SIZE, stdin);//User input is read with fgets and sent to the server with write.        
        //communication is terminated when the user enters "q" or "Q"
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break;
        
        // Send the message to the server
        str_len = write(clnt_sock, message, strlen(message)); //提前确定之后要接收的数据的大小
        recv_len = 0; //read调用的循环变量
        while (recv_len < str_len) { //只要没接收完就一直循环
            recv_cnt = read(clnt_sock, &message[recv_len], BUF_SIZE - 1);
            if (recv_cnt == -1) error_handling("read() error ! ");
            recv_len += recv_cnt;
        }
        message[recv_len] = 0;
        printf("Message from server: %s", message);
    }
    close(clnt_sock);
    return 0;
}
// Function definition for error handling
void error_handling(char* message) {
    perror(message);  // print error info
    exit(1);  // exit the program
}

运行结果

# 服务器端
[root@ECSocar ch04]#gcc echo_serv.c -o echo_serv
[root@ECSocar ch04]#./echo_serv 9190
the 1th connected client 
the 2th connected client 
    
# 客户端
[root@ECSocar ch04]#gcc echo_clnt.c -o echo_clnt
[root@ECSocar ch04]#./echo_clnt 127.0.0.1 9190
Connected!
Input message(Q to quit): hi
Message from server: hi
Input message(Q to quit): q
[root@ECSocar ch04]# ./echo_clnt 127.0.0.1 9190
Connected!
Input message(Q to quit): q

三、实现UDP回声服务器/客户端

uecho_serv.c

#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]; //Buffer for storing received and to-be-sent messages
    struct sockaddr_in serv_adr, clnt_adr; 
    socklen_t clnt_adr_sz; 
    
    if(argc != 2) { 
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }   
    
    // Creates a UDP socket.
    serv_sock = socket(PF_INET, SOCK_DGRAM, 0); 
    if(serv_sock == -1) error_handling("UDP socket() creation error");
   
    // Initialize the server address structure 
    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])); 
   
    // Set socket option SO_REUSEADDR to allow the reuse of the address.
    int on = 1;
    if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int)) < 0)  error_handling("setsockopt() error");
    
    // Bind the socket to the specified address and port.
    if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error"); 
     
    int str_len;
    // this UDP server as it is designed to run indefinitely, 
    // Enter an infinite loop to continuously receive and send data.
    while (1) { 
        clnt_adr_sz = sizeof(clnt_adr); //initialize clnt_adr_sz
        // Use recvfrom to receive data from any client (clnt_adr is populated with client's address).
        str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        //recvfrom调用会获取数据传输端的地址,并填充到clnt_adr(因此serv端知道是从哪个客户端接收的数据),正是利用该地址将接收的数据逆向传输 
        // Send the received data back to the same client
        sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz);
    }
    //没有break,也就是说是无限循环的,因此close()不会执行,没有意义
    close(serv_sock); 
    return 0;
}
void error_handling(char* message) {
    perror(message);  
    exit(1);  
}

uecho_clnt.c

#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 clnt_sock;
    char message[BUF_SIZE];
    struct sockaddr_in serv_adr, from_adr;
    socklen_t src_adr_sz;//接收数据时,数据发送端的socket地址的长度
    
    if(argc != 3) { 
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }   

    clnt_sock = socket(PF_INET, SOCK_DGRAM, 0); 
    if(clnt_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])); 
    
    int str_len;
    while (1) {
        fputs("Insert message(Q to quit): ", stdout);
        fgets(message, sizeof(message), stdin);
        //communication is terminated when the user enters "q" or "Q"
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break;
        
        // Send the message to the server
        sendto(clnt_sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
        // Receive a response from src
        src_adr_sz = sizeof(src_adr);
        str_len = recvfrom(clnt_sock, message, BUF_SIZE , 0, (struct sockaddr*)&src_adr, &src_adr_sz));
        message[str_len] = 0;
        printf("Message from server: %s", message);
    }
    close(clnt_sock);
    return 0;
}
void error_handling(char* message) {
    perror(message);  
    exit(1); 
}

运行结果

和上面TCP的几乎一样

# 服务器端
[root@ECSocar ch06]# gcc uecho_serv.c -o uecho_serv
[root@ECSocar ch06]# ./uecho_serv 9190
^C
    
# 客户端
[root@ECSocar ch06]# gcc uecho_clnt.c -o uecho_clnt
[root@ECSocar ch06]# ./uecho_clnt 127.0.0.1 9190
Insert message(Q to quit): hi,nihaoya
Message from server: hi,nihaoya
Insert message(Q to quit): how are you?
Message from server: how are you?
Insert message(Q to quit): q

四、TCP_serv & UDP_serv的差异

在这里插入图片描述

如有对函数理解不清晰的,请回到第一部分

将中间部分放在一起对比能发现:

  • tcp_serv只对clnt_adr_sz初始化一次,且是在循环前进行;而udp_serv每一次传输数据前都需要初始化clnt_adr_sz,且是在循环内部一开始就进行。书上的例子是这样写的,但是私以为这并不是TCP_serv和UDP_serv的差异,tcp_serv也应该在for内部每次accept调用前对clnt_ad_sz进行一次初始化。原因是:

    因为accept的第三个参数addrlen是value-result argument(传入传出参数),每次传入的都应该是sockaddr结构体的大小,传出客户端地址的实际大小(accept调用会修改clnt_adr_sz的值)。

    如果与下一个客户端进行连接之前不重新初始化,那么再调用accept时传入的clnt_adr_sz就是上一个客户端地址的实际大小,可能会小于我们要传出(返回)的、当前这个客户端的地址的实际大小,导致返回的客户端地址信息被截断

  • TCP连接需要listen() + accept()先listen监听连接然后accept从全连接队列中取出连接,也就是需要【建立连接】的这个过程,而UDP不需要建立连接就可以直接通信。

  • TCP接收数据时不需要指定客户端地址和地址长度,而UDP需要指定。且tcp_serv是直接从 前面调用accept()时取得的客户端socketclnt_sock) 读取数据,而udp_serv是 直接从serv_sock中读取数据,在recvfrom()调用的过程中取得客户端socket

  • 传输数据和接收数据差不多同理,就不赘述了

除了上面所展示的差异之外,TCP和UDP的实现还有很多差异,这些差异都是源于
【TCP为应用层提供可靠的、面向连接的、基于流的服务,而UDP为应用层提供不可靠的、无连接的、基于数据报的服务】。之后我们会再详细演示其它的一些差异。

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值