Linux网络编程——TCP连接实现服务端返回客户端发送的两数之和

Linux网络编程实验二

要求:

  • 服务端返回客户端发送的两个整数之和。
  • 服务器提供并发服务

建立TCP连接的基本步骤:

参考:https://blog.csdn.net/QQ1402369668/article/details/86090092

Linux网络编程——TCP连接实现服务端返回客户端发送的两数之和_第1张图片
服务端:

  • 创建监听socket套接字,套接字就相当于用于通信的工具,比如要打电话给别人,首先得获得一个手机,在代码中体现为:
int listenSocket=socket(AF_INET,SOCK_STREAM,0);

//AF_INET    :表示IPv4域
//SOCK_STREAM:表示使用TCP协议
//0          :默认参数
openSUSE 用户手册中对socket()的描述:

Linux网络编程——TCP连接实现服务端返回客户端发送的两数之和_第2张图片

  • 创建并初始化地址结构体,包括获得本机的IP地址和初始化指定的端口号
struct sockaddr_in addrSrc,addrClient;
    addrSrc.sin_family=AF_INET;
    addrSrc.sin_port=htons(6666);//服务端的监听端口
    addrSrc.sin_addr.s_addr=INADDR_ANY;//转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP
  • 绑定地址,监听端口
    //绑定
    bind(listenSocket,(struct sockaddr*)&addrSrc,sizeof(struct sockaddr_in)); 
   //监听
    listen(listenSocket,5);//5 表示最多允许和5个客户端建立TCP连接

此时服务端的监听套接字准备完毕,下一步是在该监听套接字上监听客户端的请求,在代码中体现为:在死循环中调用accept()函数,若监听到了一个客户端的连接请求,accept()函数便会返回一个新的套接字描述符,此后便用这个连接套接字与客户端进行通信。

由于要求使用并发处理,则在服务端监听到了一个请求后便会创建一个子进程与客户端通信,父进程专门用于监听TCP连接请求,子进程专门用于与客户端交互,比如进行计算等操作。

服务端完整代码:

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include


int main()
{
    //创建一个socket
    int listenSocket=socket(AF_INET,SOCK_STREAM,0);
    
    pid_t childpid;
    
    //配置ip port 协议
    struct sockaddr_in addrSrc,addrClient;
    addrSrc.sin_family=AF_INET;
    addrSrc.sin_port=htons(6666);
    addrSrc.sin_addr.s_addr=INADDR_ANY;
 
    //绑定
    bind(listenSocket,(struct sockaddr*)&addrSrc,sizeof(struct sockaddr_in));
 
    //监听
    listen(listenSocket,5);
    
    
    int connfd=0;
    int len=sizeof(struct sockaddr_in);
    
    for(;;)
    {
        connfd=accept(listenSocket,(struct sockaddr*)&addrClient,&len);
        char *ipStr=inet_ntoa(addrClient.sin_addr);//将用整数类型表示的地址转换为字符串型,便于打印观察
        printf("connect is %s\n",ipStr);
        
        if ( (childpid = fork()) == 0)// 在子进程中进行加法操作
        {   
            close(listenSocket);//子进程继承父进程的所有资源,但是子进程只负责进行加法运算,不需要使用监听套接字
            char recvBuf[100]={0};
            long int sum=0;
			
			//接受子进程发过来的两个加数
            int ret=recv(connfd,recvBuf,sizeof(recvBuf),0);
           	//打印查看结果
            printf("%s\n",recvBuf);
            //解析所接受到的两个加数,由于是以字符形式发送的,故需要转换为整数型,才能进行加法运算
            char *ptr;
            //用空格将字符串数组分隔开,分别获取两个加数,相当于python中split()函数
            ptr=strtok(recvBuf," ");
            while(ptr!=NULL)
            {
                sum+=strtol(ptr,NULL,10);
                ptr = strtok(NULL, ",");
            }
            
         	//将运算结果格式化后返回给客户端
            char Add_Result[1024]={0};
            sprintf(Add_Result,"the add result is: %ld",sum);
            send(connfd,Add_Result,strlen(Add_Result)+1,0);
            //退出子进程
        	exit(0);
        }
        //在父进程中关闭连接套接字,父进程只负责监听连接请求,剩下的处理工作只交给子进程完成,故父进程本身不需要连接套接字
        close(connfd);   
    }
}

客户端

  • 创建套接字
    服务端希望与客户端通信,首先得获得一个“通信工具”——socket套接字
  • 创建并初始化地址结构体
	struct sockaddr_in addrSrc;
    memset(&addrSrc,0,sizeof(struct sockaddr_in));
 
    addrSrc.sin_family=AF_INET;
    addrSrc.sin_port=htons(6666);
    addrSrc.sin_addr.s_addr=inet_addr("127.0.0.1");

与服务端不同的是: 地址结构体中ip地址和端口号是要访问的服务端的地址和端口号

  • 调用connect()函数进行连接
  • 连接成功后便可向服务端发送数据,并接受服务端返回的运算结果打印到屏幕上

客户端完整代码

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
 
int main(int argc,int **argv)
{
    //创建一个socket
    int clientSocket=socket(AF_INET,SOCK_STREAM,0);
    
    //配置ip port 协议
    struct sockaddr_in addrSrc;
    memset(&addrSrc,0,sizeof(struct sockaddr_in));
 
    addrSrc.sin_family=AF_INET;
    addrSrc.sin_port=htons(6666);
    addrSrc.sin_addr.s_addr=inet_addr("127.0.0.1");
 
    connect(clientSocket,(const struct sockaddr *)&addrSrc,sizeof(struct sockaddr_in));
    
    
    send(clientSocket,argv[1],strlen(argv[1]),0);
    send(clientSocket," ",1,0);//两个数之间发送一个空格,用于分割两个数
    send(clientSocket,argv[2],strlen(argv[2]),0);
    
    //接受服务端的运算结果   
    char recvBuf[1024]={0};
    recv(clientSocket,recvBuf,sizeof(recvBuf)-1,0);
    //显示到屏幕上
    printf("recv from server is :%s\n",recvBuf);
    
    //关闭套接字
    close(clientSocket);
    
    return 0;
}

运行结果:(先运行服务端,在运行客户端)
Linux网络编程——TCP连接实现服务端返回客户端发送的两数之和_第3张图片
在这里插入图片描述

改进:使用信号机制

由于父进程一直在accept()函数上监听客户端的请求,有请求时,父进程只负责3次握手建立TCP连接,之后的运算便完全交给子进程来处理,父进程还是回到监听套接字上监听,没有请求时,便阻塞在accept()函数上。当子进程运算完后便退出了,此时父进程无法正常回收子进程,导致子进程变成了系统中的僵尸进程,消耗着系统资源。

解决方法:
使用信号机制,子进程结束时,会向父进程发送一个SIGCHLD信号,但是默认情况下,父进程在收到该信号时都会忽略,不进行任何操作。

信号也称为软件中断,对应的就有中断处理函数,即可以自定义每当收到SIGCHLD信号时,父进程进行什么操作。

注意事项:
由于父进程一直在调用accept()函数,accept()函数本质上时一个慢系统调用,意思就是:它如果被信号打断的话,系统是不会再自动返回到accept()函数里面继续执行的。所以必须在程序中通过执行代码再返回到accept()中,这样父进程在中断处理函数中回收完了子进程,还可以继续回到accept()函数上继续监听。

服务端完整代码

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#include "signal.h"
#include "errno.h"
//中断处理函数,父进程若收到了SIGCHLD信号,就调用该函数回收子进程,并打印出子进程的pid号
void sig_chld(int signo)
 {
   pid_t pid;
   int stat;
   while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
    printf("child %d terminated\n", pid);
   return;
 }


int main()
{
    //创建一个socket
    int listenSocket=socket(AF_INET,SOCK_STREAM,0);
    pid_t childpid;
    
    
    //配置ip port 协议
    struct sockaddr_in addrSrc,addrClient;
    addrSrc.sin_family=AF_INET;
    addrSrc.sin_port=htons(6666);
    addrSrc.sin_addr.s_addr=INADDR_ANY;
 
    //绑定
    bind(listenSocket,(struct sockaddr*)&addrSrc,sizeof(struct sockaddr_in));
 
    //监听
    listen(listenSocket,5);
    //调用signal()函数,当收到SIGCHLD信号时,调用自己编写的sig_chld函数,注:该函数需要在accept()调用之前调用,并且只能调用一次
    signal(SIGCHLD, sig_chld);
    
    int connfd=0;
    int len=sizeof(struct sockaddr_in);
    
    for(;;)
    {
        connfd=accept(listenSocket,(struct sockaddr*)&addrClient,&len);
        
        if(connfd<0)
        {
            //由于accept()是满系统调用,判断如果是信号打断了该函数的话,则继续continue,进入下一轮循环,重新调用accept()函数
            if (errno == EINTR)
               continue; /* back to for() */
            else
               printf("accept error");    
        }

        char *ipStr=inet_ntoa(addrClient.sin_addr);
        printf("connect is %s\n",ipStr);
        
        if ( (childpid = fork()) == 0)// in the child process
        {   
            close(listenSocket);
            char recvBuf[100]={0};
            long int sum=0;

            int ret=recv(connfd,recvBuf,sizeof(recvBuf),0);
            //puts(recvBuf);
            printf("recvBuf=%s\n",recvBuf);
            char *ptr;
            ptr=strtok(recvBuf," ");
            while(ptr!=NULL)
            {
                sum+=strtol(ptr,NULL,10);
                ptr = strtok(NULL, ",");
            }
            
            printf("sum=%ld",sum);
            char Add_Result[1024]={0};
            sprintf(Add_Result,"the add result is: %ld",sum);
            send(connfd,Add_Result,strlen(Add_Result)+1,0);
            
        exit(0);
        
        }
        
        //关闭套接字in father process
        close(connfd);
        
    }
}

客户端代码同上

运行结果:

客户端发起请求,并获得了服务端返回的结果
Linux网络编程——TCP连接实现服务端返回客户端发送的两数之和_第4张图片
服务端:显示了回收的子进程pid号
Linux网络编程——TCP连接实现服务端返回客户端发送的两数之和_第5张图片

你可能感兴趣的:(Linux网络编程实验报告)