Linux操作系统 - 网络编程socket

Posted TangguTae

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux操作系统 - 网络编程socket相关的知识,希望对你有一定的参考价值。

目录

服务器端

初始化

服务器启动

测试服务器

客户端

代码改进

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、多线程版本:健壮性不强,较吃资源,效率相对较高

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

以上是关于Linux操作系统 - 网络编程socket的主要内容,如果未能解决你的问题,请参考以下文章

linux系统socket通信编程1

Linux系统编程—网络编程—socket代码实现

Linux系统与网络编程13:Socket多线程

Linux的SOCKET编程详解

Linux socket编程

linux系统UDP的socket通信编程2