Linux——网络套接字2|Tcp服务器编写

本篇博客先看后面的代码,再回来看上面这些内容。

.hpp文件,基本调用

服务器基本框架

接下来用tcp的方式创建套接字,我们用SOCK_STREAM

因为TCP是面向连接的,所以当我们通信的时候需要建立连接,

listen

将套接字状态转为监听状态,第一个参数套接字,第二个参数全连接队列长度(后续会详解这个参数)。

成功返回0,否则返回-1

如何理解监听状态?

由于TCP通信是需要连接的,即如果对方发过来连接我们要即使进行接收,listen是监听来自客户端的tcp socket的连接请求,即listen是看看客户端有没有发送连接请求过来。

我们在tcp_server.hpp里面完成创建tcp时初始化服务器的三大步:1.创建套接2.绑定3.listen

当我们启动服务器之后,用netstat -antp 可查看tcp相关的一些服务,加l之后,只会显示监听状态的服务,下图中listen表示当前服务器处于监听状态,即此时服务器随时等待别人的请求。

当服务器启动之后要先获取连接,有人连服务器时,服务器才能获取连接,若没人连服务器,服务器就处于阻塞状态,这里我们要这个接口。

accept

获取新连接,第一个参数是套接字,第二个参数是输出型参数,第三个参数是输入输出型参数。

accept成功之后返回一个套接字,失败返回-1

后俩个参数和recvfrom后俩个参数含义一模一样,代表的是客户端的ip和客户端的端口号

accept的返回值sock和我们自己的_sock有何区别?

_sock的核心任务只有一个:获取新连接。而未来真正进行网络服务的是sock。_sock只是帮助底层的accept把底层的连接获取上来。这个_sock我们更倾向于称作listensock,也俗称监听套接字。上图里得sock一般成为servicesock,服务套接字。

_sock是通过accept获取新的连接,sock负责网络服务。

read|write

当服务器创建连接成功后,服务器需要读取对方发送过来的信息,注意:这里不能用recvfrom这个专门用于udp报文,是面向数据报的,我们用下面这个接口。这个就是我们之前在OS经常用到的接口。

单进程循环版

我们可用telnet+IP+端口号去连接我们的服务器

之后按ctrl+],就可以发消息,此时发送的消息会被服务器收到

telnet的发送完信息之后,会立马读取对方发过来的信息,我们这里让服务器收到信息后,把信息又返回给了对方,因此这里左边消息出现了俩次。

telnet退出的时候输入按ctrl+],之后输入quit.此时telnet会自动退出,根据我们所写的程序,服务器也会自动退出。

当我们创建俩个客户端,给服务器发信息时,第二个客户端发不出去。

当第一个客户端退出之后,第二个客户端发送的消息会立刻被服务器接收。

这种情况又如何解决呢?这里我们写的是单进程,而且service里面是死循环,死循环一直在进行读取,若不退出就一直在读写。我们可以写一个多进程版

创建子进程,让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?

能,创建子进程时,子进程会继承父进程的文件描述符表,所以会和父进程看到同一个文件。

这里让父进程创建连接,子进程去给客户端提供服务。

但是当子进程退出之后会进入僵尸状态,父进程需要回收子进程,而父进程在等待子进程时是阻塞时等待,子进程不退出,父进程wait就不会返回,但由于这里有多个客户端,就不能使用waitpid,我们需要一种不阻塞式的等待子进程。如果这里子进程较多,用waitpid非阻塞等待比较麻烦。

子进程退出时会向父进程发送SIGCHILD信号,如果主动忽略SIGCHILE信号,子进程退出的时候会自动释放自己的僵尸状态。

父进程至少打开了这俩个套接字,即俩个文件描述符。

创建了子进程,子进程会继承父进程打开的文件与文件fd。

这里子进程是来进行提供服务的,不需要知道listensock,只需要servicesock,因此要让子进程关闭掉不需要的listensock,对于父进程只需要关闭自己不需要的servicesock套接字。父进程永远只保留listensock即可。

tcp_server.hpp

#pragma once
#include<iostream>
#include<cassert>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string>
#include<signal.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<memory>
#include<cerrno>
#include<cstring>
#include"log.hpp"
static void service(int sock,const std::string&clientip,const uint16_t&clientport)
{
    //收到对方的消息后,进行一些转换再返回给对方
    char buffer[1024];
    while(true)
    {
        //read和write可以直接被使用!
        ssize_t s=read(sock,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;
            //将发过来的数据当作字符串
            std::cout<<clientip<<":"<<clientport<<"# "<<buffer<<std::endl;
        }
        else if(s==0)
            //返回值为0,代表对方关闭了连接,啥都没读到
        {
            logMessage(NORMAL,"%s:%d shutdown,me too!",clientip.c_str(),clientport);
            break;
        }
        else
        {
            logMessage(ERROR,"read socket error,%d:%s",errno,strerror(errno));
            break;
        }
        //走到这就读取成功了,我们接下来向套接字当中写入
        write(sock,buffer,strlen(buffer));
    }
}
class TcpServer
{
    private:
        const static int gbacklog=20;//这是listen的第二个参数
//一般不能太大也不能太小,后面会解释,这时listen的第二个参数
    public:
        TcpServer(uint16_t port,std::string ip=""):listensock(-1),_port(port),_ip(ip)
        {}
        //初始化服务器
        void initServer()
        {
            listensock=socket(AF_INET,SOCK_STREAM,0);//返回值照样是文件描述符
            //参数含义第一个:网络通信 第二个:流式通信
            if(listensock<0)//创建套接字失败
            {
                logMessage(FATAL,"create socket error,%d:%s",errno,strerror(errno));//打印报错信息
                exit(2);
            }
            logMessage(NORMAL,"create socket success,sock:%d",listensock);//打印套接字,它的文件描述符是3
            //到这里创建套接字成功,接下来进行bind
            //bind目的是让IP和端口进行绑定
            //我们需要套接字,和sockaddr(这个里面包含家族等名称)
            //绑定——文件和网络
            struct sockaddr_in local;
            memset(&local,0,sizeof local);//初始化local
            local.sin_family=AF_INET;
            local.sin_port=htons(_port);//端口号,端口是主机序列,我们需要主机转网络
            local.sin_addr.s_addr=_ip.empty()?INADDR_ANY:inet_addr(_ip.c_str());
            //IP地址,由于我们构造的时候是IP是个空的字符串
            //所以我们可以绑定任意IP
            //我们一般推荐绑定0号地址或特殊IP
            //填充的时候IP是空的,就用INADDR_ANY否则用inet_addr
            if(bind(listensock,(struct sockaddr*)&local,sizeof local)<0)
            {
                //走到这就绑定失败了,我们打印错误信息
                logMessage(FATAL,"bind error,%d:%s",errno,strerror(errno));
                exit(3);
            }
            //因为TCP是面向连接的,当我们正式通信的时候需要先建立连接。
            //listen是在等待对方发过来连接,我们直接接收
            if(listen(listensock,gbacklog)<0)
            {
                logMessage(FATAL,"listen error,%d:%s",errno,strerror(errno));
                exit(4);
            }
            logMessage(NORMAL,"init server success");
        }
        //启动服务器
        void start()
        {
            signal(SIGCHLD,SIG_IGN);//子进程自动释放自己的僵尸状态,用信号可以避免父进程阻塞等待子进程
            //子进程退出,会向父进程发SIGCHLD信号,对SIGCHLD信号主动忽略,子进程退出的时候,会自动释放自己的僵尸状态
            //服务器一旦启动就要周而复始的去运行
            while(true)
            {
                //服务器启动之后先获取连接
                //当有人连服务器的时候才能获取连接,若没人连就无法获取
                //这里当没人连服务器的时候,我们让服务器一直进行阻塞。
                struct sockaddr_in src;
                socklen_t len=sizeof(src);
                int servicesock=accept(listensock,(struct sockaddr*)&src,&len);
                if(servicesock<0)
                {
                    //获取连接失败
                    logMessage(ERROR,"accept error,%d:%s",errno,strerror(errno));
                    continue;
                }
                //获取连接成功了
                uint16_t client_port=ntohs(src.sin_port);
                //客户端端口号在src
                //由于是网络发送过来得套接字信息
                //所以要把信息进行网络转主机
                std::string client_ip=inet_ntoa(src.sin_addr);
                //我们需要将四字节网络序列的IP地址,转换成字符串风格的点分十进制的IP地址
                //到这里我们拿到了IP和端口号
                //谁连接服务器,服务器就拿到谁的信息
                logMessage(NORMAL,"link success,servicesock:%d | %s : %d| \n",servicesock,client_ip.c_str(),client_port);
                //这里servicesock是4,符合数组下标描述符的分配规则
                //接下来进行通信服务
                //verison1--单进程循环版——智能一次处理一个客户端,处理完一个客户端之后,才能处理下一个客户端
                //service(servicesock,client_ip,client_port);
                //version——2.1多进程版
                //创建子进程,让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?
                pid_t id=fork();
                assert(id!=-1);
                if(id==0)
                {
                    //子进程
                    close(listensock);
                    //子进程只是来进行提供服务的,我们关闭它不需要的套接字,同理父进程也关闭自己不需要的套接字
                    service(servicesock,client_ip,client_port);//子进程开始提供服务
                    exit(0);//子进程退出进入僵尸状态
                }
                //父进程要回收子进程
                close(servicesock);
            }
        }
        ~TcpServer(){}
    private:
        uint16_t _port;//端口号
        std::string _ip;//ip地址
            int listensock;
};

tcp_server.cc

#include "tcp_server.hpp"
#include <memory>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}

// ./tcp_server port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    svr->initServer();
    svr->start();
    return 0;
}

makefile

.PHONY:all
all:tcp_client tcp_server

tcp_client:tcp_client.cc
    g++ -o $@ $^ -std=c++11 #-lpthread
tcp_server:tcp_server.cc
    g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
    rm -f tcp_client tcp_server

log.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./threadpool.log"

// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if(level== DEBUG) return;
#endif
    // va_list ap;
    // va_start(ap, format);
    // while()
    // int x = va_arg(ap, int);
    // va_end(ap); //ap=nullptr
    char stdBuffer[1024]; //标准部分
    time_t timestamp = time(nullptr);
    // struct tm *localtime = localtime(&timestamp);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024]; //自定义部分
    va_list args;
    va_start(args, format);
    // vprintf(format, args);
    vsnprintf(logBuffer, sizeof logBuffer, format, args);
    va_end(args);

    // FILE *fp = fopen(LOGFILE, "a");
    printf("%s%s\n", stdBuffer, logBuffer);
    // fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    // fclose(fp);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
当我们在编写基于TCP的应用程序时,操作系统提供了许多套接字API来帮助我们实现网络通信。 首先,操作系统提供了套接字的创建函数。我们可以使用该函数在应用程序中创建套接字,并返回一个唯一标识符,也称为套接字描述符。这个套接字描述符用来标识我们创建的套接字。 其次,操作系统提供了套接字的绑定函数。我们可以使用该函数将套接字与特定的IP地址和端口号绑定在一起。这样,我们就可以通过使用指定的IP地址和端口号来访问套接字。 接下来,操作系统提供了套接字的监听函数。我们可以使用该函数将套接字设置为监听状态,以便接收来自其他计算机的连接请求。一旦有连接请求到达,操作系统会将其排队等待处理。 然后,操作系统提供了套接字的接受函数。我们可以使用该函数从队列中接受连接请求,并创建一个新的套接字来与客户端进行通信。这个新的套接字将作为与客户端通信的通道。 此外,操作系统还提供了套接字的发送和接收函数。我们可以使用这些函数发送和接收数据,实现应用程序之间的通信。这些函数提供了不同的选项来控制发送和接收的数据。 最后,操作系统提供了套接字的关闭函数。我们可以使用该函数来关闭套接字,释放与之相关的资源。 综上所述,操作系统提供了诸多套接字API,包括创建、绑定、监听、接受、发送、接收和关闭函数等,帮助我们在编写TCP应用程序时进行网络通信的实现。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

头发没有代码多

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值