Linux操作系统 - 网络编程socket(2)

目录

服务器端

初始化

服务器启动

测试服务器

客户端

代码改进

1、多进程版本

2、多线程版本

三种版本的比较


之前有讲过基于UDP的网络编程一些基础的知识,现在看看基于TCP的网络编程。

首先TCP与UDP最大的不同是TCP是面向连接的,可靠传输。所以在编程实现方面有很多不同的地方,接下来看看具体的细节。

服务器端

在初始化的过程中,与UDP的方式有很多相同的地方,例如创建套接字,绑定端口号。而在TCP除了上述的操作以外还需要进行监听,即把套接字变成监听套接字。

初始化

class tcpServer
{
  private:
    int _port;
    int _lsock;
  public:
    tcpServer(int port = 8080)
      :_port(port)
    {}
    //初始化
    void initServer()
    {
      _lsock = socket(AF_INET,SOCK_STREAM,0);//创建套接字
      if(_lsock < 0)
      {
        cerr<<"sock error!"<<endl;
        exit(1);
      }
      sockaddr_in local;
      local.sin_family = AF_INET;
      local.sin_port = htons(_port);
      local.sin_addr.s_addr = INADDR_ANY;
      if(bind(_lsock,(struct sockaddr*)&local,sizeof(local)) < 0)//绑定端口和ip地址
      {
        cerr<<"bind error!"<<endl;
        exit(2);
      }
      //进行监听
      if(listen(_lsock,5) < 0)//队列长度设置为5(实际底层大小为6)
      {                                                                       
        cerr<<"listen error!"<<endl;
        exit(3);
      }
    }
};

listen 函数

listen函数将套接字变为监听套接字,第一个参数就是传递创建好的套接字,第二个参数使用来确定最大接收连接的数量,这个地方涉及到全连接队列的知识,在后面说TCP协议的细节的时候在说一下这个参数,本身的目的是让系统效率最大化,不过这个参数不能太大。

服务器启动

当服务器启动之后需要接收来自客户端的连接。

accept函数

第一个参数是监听套接字(listen过后的套接字),第二个参数、第三个参数是输出型参数。

返回值

注意:返回值是一个文件描述符,也是一个socket,很关键,这个文件描述符就是实际通信过程中所需的文件描述符。

如果队列上不存在挂起的连接,并且套接字未标记为非阻塞,则accept()会阻塞调用方,直到存在连接为止。如果套接字标记为非阻塞,并且队列中不存在挂起的连接,则accept()将失败,并出现错误EAGAIN或EWOLDBLOCK。

void start()
{
  sockaddr_in remote;
  socklen_t len = sizeof(remote);
  while(true)
  {
    int sock = accept(_lsock,(struct sockaddr*)&remote,&len);
    if(sock < 0)
    {
      cerr<<"accept error"<<endl;
      continue;//这里失败不退出,继续等待连接
    }
    cout<<"get a new link"<<endl;
    service(sock);//对连接进行处理
  }
}

当获得到连接之后,需要对连接进行处理,自定义一下处理动作

void service(int sock)
{
  char buf[1024];
  while(true)                                                 
  {
    ssize_t s = recv(sock,buf,sizeof(buf)-1,0);
    if(s > 0)
    {
      buf[s] = '\0';
      cout<<buf<<endl;//对收到的信息打印到命令行
      string echo(buf);
      echo+=" echo Server";
      send(sock,echo.c_str(),echo.size(),0);//返回给客户端
    }
    else if(s == 0)//读到0表示客户端已经退出
    {
      cout<<"client is quit!"<<endl;
      break;
    }
    else 
    {
      cout<<"recv error!"<<endl;
    }
  }
  close(sock);//记得结束后关闭文件描述符
}

其中有两个函数,一个recv函数,一个是send函数。

recv函数

在UDP里面讲过一个recvfrom函数,recvfrom函数针对UDP,需要告知是谁发过来的,有一个输出型的参数,而recv没有,因为TCP在接收连接(accept)的时候已经知道是谁发过来的,所以可以不用recvfrom函数。

参数分别是套接字、接收数据buf、期望接收的大小和阻塞接收标志flag。flag=0表示阻塞。

返回值

返回值为实际读到多少字节的数据,读到0表示对方已经断开连接,读到-1表示出错。

send函数

和sendto是一类接口,不过send是面向TCP的,不需要指名发给谁,因为TCP已经把连接建立好了,文件描述符在底层绑定了对方的ip地址和端口号。

可以发现这一批接口和系统IO中的read和write很相似,此时这两组接口都是面向字节流的(TCP就是面向字节流的),也就是说我们用read和write也可以直接读取套接字里面的数据或者往套接字里面写入数据。

int main(int argc,char* argv[])
{
  if(argc != 2)
  {
    cerr<<"parameter error!"<<endl;
    exit(1);
  }
  tcpServer *us = new tcpServer(atoi(argv[1]));    
  us->initServer();
  us->start();
  return 0;
}

测试服务器

运行一下

 

 

补充一个工具telnet

telnet是远程终端协议,是TCP/IP协议家族的成员之一,默认端口23。用来测试网络。

用telnet工具来作为客户端测试一下服务器。

首先需要安装一下:sudo yum install telnet telnet-server

 

退出就是在telnet命令模式下ctrl ]  输入quit

 

客户端

与UDP不同的是,客户端需要发起连接

connect函数

第一个参数是文件描述符,第二个第三个参数想必已经不陌生了,需要对方的ip地址和端口号信息,以结构体的形式传递参数。

class tcpClient
{                                                                        
  private:
    string _ip;
    int _port;
    int sock;
  public:
    tcpClient(string ip = "127.0.0.1",int port = 8080)
      :_ip(ip)
      ,_port(port)
    {}
    void initClient()
    {
      sock = socket(AF_INET,SOCK_STREAM,0);//SOCK_STREAM面向字节流
      if(sock < 0)
      {
        cerr<<"sock error!"<<endl;
        exit(1);
      }

      struct sockaddr_in remote;//绑定目的ip地址与端口号
      remote.sin_family = AF_INET;
      remote.sin_port = htons(_port);
      remote.sin_addr.s_addr = inet_addr(_ip.c_str());
      if(connect(sock,(struct sockaddr*)&remote,sizeof(remote)) != 0)//连接请求
      {
        cerr<<"connect error!"<<endl;
        exit(2);
      }
    }
};

初始化完成后进行通信,这里就比较简单,自定义函数。

void start()
{
  while(true)
  {
    string msg;
    cout<<"please enter msg# ";
    cin>>msg;
    send(sock,msg.c_str(),msg.size(),0);
    char echo[128] = {'\0'};
    ssize_t s = recv(sock,echo,sizeof(echo)-1,0);
    if(s > 0)
    {
      echo[s] = '\0';
      cout<<"get msg from Server: "<<echo<<endl;
    }
  }
}

主函数

int main(int argc,char* argv[])
{
  if(argc != 3)
  {
    cerr<<"parameter error!"<<endl;
    exit(1);
  }
  tcpClient *uc = new tcpClient(argv[1],atoi(argv[2]));
                                                            
  uc->initClient();
  uc->start();
  return 0;
}

运行结果 

 

上述程序面临的一些问题

面对多个请求如何实现?

代码改进

只针对服务器

1、多进程版本

修改一下start函数

void start()
{
  sockaddr_in remote;
  socklen_t len = sizeof(remote);
  while(true)
  {
    int sock = accept(_lsock,(struct sockaddr*)&remote,&len);
    if(sock < 0)
    {
      cerr<<"accept error\n"<<endl;
      continue;
    }
    pid_t id = fork();
    if(id == 0)
    {
      close(_lsock); //子进程不关心监听套接字,直接关闭
      service(sock); 
      exit(0);
    }
    close(sock);//由于交给子进程处理,父进程不需要关心sock
    cout<<"get a new link"<<endl;
    //子进程退出,一些方法回收资源
    //1、父进程等待(不能阻塞等待)
    //2、自定义捕捉信号SIGCHLD
    //3、将SIGCHLD信号忽略                                     
  }
}

这里有很多细节

1、由于子进程会按照父进程的模板来创建,所以文件描述符资源也对应相同,子进程需要关闭一些自己并不关心的文件描述符资源,父进程也是如此。

2、子进程的退出,子进程的资源需要回收,此时需要父进程来处理(注意,不是父进程回收,父进程只是发起回收这个动作,实质上是内核完成资源的回收)。一般来说父进程需要等待(wait/waitpid)。还有一些其他的方法,比如说自定义捕捉(针对SIGCHLD信号),或者直接忽略,将资源交给内核回收。

忽略比较简单,在初始化那里加一行代码 signal(SIGCHLD,SIG_IGN);

测试:

同时有三个连接请求,将前两个进程放在后台

查看当前进程信息,三个客户端Client进程,三个子进程Server在处理连接,一个父进程Server。

这样服务器就可以同时应对多个连接。

但是多进程的方式资源消耗比较大,且进程间的切换开销也很大。引入多线程版本。

2、多线程版本

static void* service_routine(void* arg)
{
  pthread_detach(pthread_self());//分离线程,避免主线程阻塞等待释放资源
  cout<<"creat thread successfully,tid is "<<pthread_self()<<endl;
  int *p = (int*)arg;
  service(*p);//此时service也需要是静态函数
  return nullptr;
}
void start()
{                                                                               
  sockaddr_in remote;
  socklen_t len = sizeof(remote);
  while(true)
  {
    int sock = accept(_lsock,(struct sockaddr*)&remote,&len);
    if(sock < 0)
    {
      cerr<<"accept error\n"<<endl;
      continue;
    }
    cout<<"get a new link"<<endl;
    pthread_t tid;
    pthread_create(&tid,nullptr,service_routine,(void*)&sock);//创建线程去执行通信
  }
}

需要注意的几个点

1、由于在类里面,线程处理的函数得是静态函数,因为函数参数这里有一个隐藏的this指针,所以可以处理成静态函数的方式舍弃this指针,由于静态函数没有this指针,所以也无法调用类里面的service函数,此时也要把service设置为静态函数。

2、线程退出时也需要主线程释放资源,如果用pthread_join函数去释放资源,主线程会陷入阻塞状态,在上一个线程为退出的状态下,无法接收新的连接,所以可以采用分离线程的方式。

三种版本的比较

1、单进程:一般不使用

2、多进程版本:健壮性强,比较吃资源,效率低下

3、多线程版本:健壮性不强,较吃资源,效率相对较高

当大量客户端需要接入时,系统会存在大量的执行流。此时切换是影响效率的重要原因

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值