TCP/IP网络编程 第四章:实现基于TCP的服务器端/客户端

进入等待连接状态

在之前的章节中,我们已经学习到了如何创建一个套接字,并且如何对套接字绑定IP地址和端口的内容,接下来就要通过调用listen函数进入连接等待状态,只有在调用了listen函数后客户端才可以调用connect函数。

#include<sys/socket.h>
int listen (int sock,int backlog);//成功时返回0,失败时返回-1
    sock    //希望进入等待连接请求状态的套接字文件描述符,传递的套接字参数称为服务器端监听套接字
    backlog //连接请求等待队列的长度      

先解释一下上述的各个名词,首先是服务器端监听套接字,其实很好理解,客户端的连接请求也是一种网络中到来的一种数据,我们总的有个一开始的套接字来接受这种类型的网络数据,才能进行接下来的数据传输。再来解释一下等待队列的长度,等待队列就是说客户端请求的队列,那么长度就是最多有多少个客户端请求可以在这个队列中

客户端如果向服务器端询问:“请问我是否可以发起连接?”服务器端套接字就会亲切应答:“您好!当然可以,但系统正忙,请到等候室排号等待,准备好后会立即受理您的连接。”同时将连接请求请到等候室。调用listen函数即可生成这种门卫(服务器端套接字),listen函数的第二个参数决定了等候室的大小。等候室称为连接请求等待队列,准备好服务器端套接字和连接请求等待队列后,这种可接收连接请求的状态称为等待连接请求状态。

受理客户端连接请求

在受理请求之后就可以进入数据交换的状态了。那么是什么东西来进行实际的数据交换呢?不就是套接字嘛,那么肯定不是监听套接字本身进行数据交换,因为它还要负责接下来的客户端请求受理。那此时就需要另外一个套接字,接下来的函数将亲自创建套接字,并连接到发起请求的客户端。

#include<sys/socket.h>
int accept(int sock,struct sockaddr* addr,socketlen_t *addrlen);
//成功时返回创建的套接字文件描述符,失败时返回-1
    sock       //服务器端监听套接字的文件描述符
    addr       //保存发起请求的客户端地址信息的变量地址值
    addrlen    //保存客户端地址长度,即第二个变量的长度

函数调用成功后,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符。

TCP客户端的默认函数调用顺序

与服务器端相比,区别就在于"请求连接",它是创建客户端套接字后向服务器端发起的连接请求。通过调用如下函数完成。

#include<sys/socket.h>
int connect (int sock,struct socketaddr* servaddr,socketlen_t addrlen);
//成功时返回0,失败时返回-1
    sock     //客户端套接字文件描述符
    servaddr //保存目标服务器地址信息的变量地址值
    addrlen  //第二个结构体参数servadddr的变量地址长度(以字节为单位)

上述函数在遇到一下情况之一才会返回(完成函数调用)

情况一:服务器端接受连接请求

情况二:发生断网等异常情况而中断连接请求

实现迭代服务器端/客户端

在我们之前实现服务器端/客户端代码都是完成了一个连接后,就直接断掉了。这个行为显然是不合理的,作为服务器端你怎么只用连接一次呢?我们接下来实现的迭代服务器端/客户端实际上只不过是反复受理等待队列中的客户端。注意,此时实现的是服务器在同一时刻只可以服务于一个客户端,在接下来的章节会讲述多进程和多线程。

迭代回声服务器端/客户端

下面是程序的基本运行方式:

□服务器端在同一时刻只与一个客户端相连,并提供回声服务。

□ 服务器端依次向5个客户端提供服务并退出。

□客户端接收用户输入的字符串并发送到服务器端。

□服务器端将接收的字符串数据传回客户端,即“回声”。

□服务器端与客户端之间的字符串回声一直执行到客户端输入Q为止。

下面是服务端的代码

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>

#defind BUF_SIZE 1024
void error_handling(char*message);

int main(int argc,char* argv[]){
    int serv_sock,clnt_sock;
    char message[BUF_SIZE];
    int str_len,i;
   
    struct socketaddr_in serv_addr,clnt_addr;
    socklen_t clnt_addr_sz;

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

    serv_sock=socket(PF_INET,SOCE_STREAM.0);
    if(serv_sock==-1)error_handling("socket() argv[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]));

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

    if(listen(serv_sock,5)==-1)
       error_handling("listen() error");

    clnt_addr_sz=sizeof(clnt_addr);
    for(int i=0;i<5;++i){
        clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_sz);
        if(clnt_sock==-1)
           error_handling("accept() error");
        else
           printf("Conneted client %d \n",i+1);

    while((str_len=read(clnt_sock,message,BUF_SIZE))!=0)
        write(clnt_sock,message,str_len);

    close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}
voie 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>

#defind BUF_SIZE 1024
void error_handling(char *message);

int main(int argc,char*argv[]){
    int sock;
    char message[BUF_SIZE];
    int str_len;
    struct sockaddr_in serv_addr;

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

    sock=socket(PF_INET,SOCK_STREAM,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]));

    if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
       error_handling("connect() error!");
    else 
       puts("Connected.........");

    while(1){
       fputs("Input message(Q to quit):",stdout);
       fgets(message, BUF_SIZE, stdin);
       if(!strcmp(message,"q\n") || !strcmp(message,"o\n"))
       break;

       write(sock,message,strlen(message));
       str_len=read(sock, message, BUF_SIZE-1);
       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);
}

回声客户端存在的问题

write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE - 1);
message[str_len]=0;
printf("Message from server: %s",message);

以上代码有个错误假设:“每次调用read、write函数时都会以字符串为单位执行实际的I/O操作。”
当然,每次调用write函数都会传递1个字符串,因此这种假设在某种程度上也算合理。但大家还记得第2章中“TCP不存在数据边界”的内容吗?还需考虑服务器端的如下情况:“字符串太长,需要分2个数据包发送!”

服务器端希望通过调用1次write函数传输数据,但如果数据太大,操作系统就有可能把数据分成多个数据包发送到客户端。另外,在此过程中,客户端有可能在尚未收到全部数据包时就调用read函数。
“但上述示例不是正常运转了吗?”
当然,我们的回声服务器端/客户端给出的结果是正确的。但这只是运气好罢了!只是因为收发的数据小,而且运行环境为同一台计算机或相邻的两台计算机,所以没发生错误,可实际上仍存在发生错误的可能。

基于Windows的实现

基于Windows的回声服务器端

为了将Linux平台下的示例转化成Windows平台示例,需要记住以下4点

□通过WSAStartup、WSACleanup函数初始化并清除套接字相关库。

□ 把数据类型和变量名切换为Windows风格。
□ 数据传输中用recv、send函数而非read、write函数。
□关闭套接字时用closesocket函数而非close函数。

猜你喜欢

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