网络编程套接字(Socket通信)

网络编程套接字(Socket通信)

本节重点:
1、认识IP地址, 端口号, 网络字节序等网络编程中的基本概念;
2、学习socket api的基本用法;
3、能够实现一个简单的udp客户端/服务器;
4、能够实现一个简单的tcp客户端/服务器(单连接版本, 多进程版本, 多线程版本);
5、理解tcp服务器建立连接, 发送数据, 断开连接的流程

目录

  • 预备知识
    • IP地址和端口号(port)
    • 理解 "端口号" 和 "进程PID"
    • TCP协议与UDP协议
    • 网络字节序
  • UDP通信
    • socket编程接口
  • TCP通信
    • TCP系统调用接口介绍
    • TCP单进程通信
    • TCP多进程通信
    • TCP多线程通信
    • 文件描述符与Socket的联系

预备知识

IP地址和端口号(port)

IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
举个例子:唐僧西天取经,总喜欢说一句话,我从东土大唐而来,要到西方大雷音寺拜佛取经
源IP地址:就相当于东土大唐
目的IP地址:就相当于大雷音寺

端口号(port):
1、端口号(port)是传输层协议的内容.
2、端口号是一个2字节16位的整数;
3、端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
4、一个端口号只能被一个进程占用
例:
当唐僧到达西天后,需要有人拿真经给他,这个人就是阿南和迦叶,阿南和迦叶就相当于端口号。

IP地址标识了硬件的唯一性,IP地址用来确认公网中的唯一一台主机,port用来标识该主机上唯一的进程

IP+port标识的是全网内唯一的进程

理解 “端口号” 和 “进程PID”

port和进程pid都能唯一标识一个进程,那么他们之间后什么关系呢?

端口号和pid的关系:不是所有的进程都是网络进程,所以不是所有的进程都需要端口号,但是所有的进程都需要pid,只有网络进程才需要端口号,pid相当于身份证号,端口号相当于学号。

TCP协议与UDP协议

这里我们简单了解一下TCP和UDP协议,后面会有博客详细阐述

TCP:
传输层协议
有连接
可靠传输
面向字节流

UDP
传输层协议
无连接
不可靠传输
面向数据报

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

1、发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;

2、接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;

3、因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.

4、TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.

不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;

5、如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

总结:我们的电脑一般是小端机,即低地址放低权值的数据,高地址放高权值的数据,而网络中采用大端字节序,所以我们发送数据前,需要将发送的数据转成大端发送

例:
网络编程套接字(Socket通信)_第1张图片

先发送12 最后发送 cd

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
网络编程套接字(Socket通信)_第2张图片
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;

如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

UDP通信

socket编程接口

socket是创建套接字的接口
网络编程套接字(Socket通信)_第3张图片
实现一个简单的基于socket的UDP通信
由客服端client给服务器server发送消息,server再将消息回显给client.
代码实现

Makefile文件

1 .PHONY:all
  2 all:udpClient udpServer
  3 
  4 udpClient:udpClient.cc
  5   g++ -o $@ $^ -std=c++11 
  6 udpServer:udpServer.cc
  7   g++ -o $@ $^
  8 
  9 .PHONY:clean
 10 clean:
 11   rm -f udpClient udpServer  

udpServer.hpp文件

1 #pragma once                                                                                                        
  2 
  3 #include<iostream>
  4 #include<string>
  5 #include<sys/types.h>
  6 #include<sys/socket.h>
  7 #include<arpa/inet.h>
  8 #include<netinet/in.h>
  9 #include<stdlib.h>
 10 #include<unistd.h>
 11 class udpServer
 12 {
 13   private:
 14     std::string ip;
 15     int port;
 16     int sock;
 17   public:
 18     udpServer(std::string _ip="127.0.0.1",int _port=8080)
 19       :ip(_ip)
 20       ,port(_port)
 21   {
 22 
 23   }
 24     void initServer()
 25     {
 26       //socket使创建套接字的接口
 27       // AF_INET:是底层使用的某种协议,这里是 ipv4.SOCK_DGRAM:是所创建的套接字的类别,SOCK_DGRAM 
 28       // 是指UDP.SOCK_STREAM是TCP.  最后一个参数 0 ,是采用的协议,一般默认为0就行 。返回值是一个文件
 29       // 描述符,一般从3开始
 30       sock=socket(AF_INET,SOCK_DGRAM,0);
 31       std::cout<< "sock: "<< sock <<std::endl;
 32       struct sockaddr_in local;
 33       local.sin_family =AF_INET; //sin.family是指协议家族,这里用ipv4
 34       local.sin_port = htons(port); //端口号 ,要主机转一下网络序列  
 35       local.sin_addr.s_addr=inet_addr(ip.c_str()); //inet_addr 把字符串的ip地址,转为4字节的网络序列
 36      
 37       //bind绑定,是使内存文件与网络信息关联起来
 38       //第一个参数sock是刚刚所创建的套接字,第二个参数local是ip+port,第3个是所传地址的长度
 39       if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)  // <0 绑定失败
 40       {
 41         std::cerr<< "bind error!\n"<<std::endl;  //cerr标准错误                                                     
 42         exit(1);
 43       }
 44     }
 45 
 46     //启动服务器,简单的回应服务器
 47     //实现从网络中接收数据,然后把收到的数据在本地打印一下,再把该数据返回给客服端
 48     void start()
 49     {
 50       char msg[64];
 51       for(;;)
 52       {                                                                                                             
 53         msg[0]='\0';// 清空字符串
 54        struct sockaddr_in end_point;  //发送方
 55        socklen_t len=sizeof(end_point);  //发送方地址大小
 56 
 57        //recvfrom 第一个参数sock是套接字,第二个参数msg是从哪里读,第3个参数是期望读多少,第4个参数设置为
 58        //是否非阻塞,为0为阻塞,第5个参数是谁发的,第6个参数是发多少。返回值是读到了多少字节,返回-1为错误
 59        ssize_t s=recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);
 60 
 61        if(s > 0)
 62        {
 63          msg[s]='\0'; //s中最后一位设置为 '\0'
 64          std::cout<<"client# " <<msg <<std::endl;
 65          std::string echo_string=msg;
 66          echo_string+= " [server echo!]"; //服务器回显
 67          //sendto发送,参数与recvfrom 类似
 68          sendto(sock,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&end_point,len);
 69        }
 70       }
 71     }
 72     ~udpServer()
 73     {
 74       close(sock);
 75     }
 76 };                                         

网络编程套接字(Socket通信)_第4张图片
udpServer.cc文件

  1 #include "udpServer.hpp"
  2 
  3 int main()
  4 {
  5   udpServer *up=new udpServer();
  6   up->initServer();
  7   up->start();                                                                                                                    
  8   delete up;
  9   return 0;
 10 }
~

网络编程套接字(Socket通信)_第5张图片
udpClient.hpp文件

 1 #pragma once                                                                                                                      
  2 
  3 #include<iostream>
  4 #include<string>
  5 #include<sys/types.h>
  6 #include<sys/socket.h>
  7 #include<arpa/inet.h>
  8 #include<netinet/in.h>
  9 #include<stdlib.h>
 10 #include<unistd.h>
 11 class udpClient
 12 {
 13   private:
 14     std::string ip;
 15     int port;
 16     int sock;
 17   public:
 18     //ip,port填谁的,填服务器的
 19     udpClient(std::string _ip="127.0.0.1",int _port=8080)
 20       :ip(_ip)
 21       ,port(_port)
 22   {
 23 
 24   }
 25     void initClient()
 26     {
 27       sock=socket(AF_INET,SOCK_DGRAM,0);
 28       std::cout<< "sock: "<< sock <<std::endl;
 29     }
 30 
 31     //启动服务器,简单的回应服务器
 32     void start()
 33     {
 34       //char msg[64];
 35       std::string msg;
 36       struct sockaddr_in peer;
 37       peer.sin_family=AF_INET;
 38       peer.sin_port=htons(port); //服务端port
 39       peer.sin_addr.s_addr=inet_addr(ip.c_str());  //服务端的ip
 40       for(;;)
 41       {
 42           std::cout<< "Please Enter# ";
 43           std::cin>>msg;
 44           if(msg=="quit")
 45           {
 46             break;
 47           }
 48           //发送数据
 49           sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));
 50                                                                                                                                   
 51           char echo[128];   //接收数据存储区
 52           
 53           ssize_t s=recvfrom(sock,echo,sizeof(echo)-1,0,nullptr,nullptr);
 54           if(s>0)
 55           {
 56             echo[s]=0;
 57             std::cout<< "server# "<< echo <<std::endl;
 58           }
 59       }
 60     }
 61     ~udpClient()
 62     {
 63       close(sock);
 64     }
 65 };                                    


网络编程套接字(Socket通信)_第6张图片
udpClient.cc文件

  1 #include "udpClient.hpp"
  2 
  3 int main()
  4 {
  5   udpClient uc;
  6   uc.initClient();
  7   uc.start();                                                                                                                     
  8   return 0;
  9 }
~

网络编程套接字(Socket通信)_第7张图片
成果演示:
网络编程套接字(Socket通信)_第8张图片

TCP通信

TCP系统调用接口介绍

socket:
网络编程套接字(Socket通信)_第9张图片

bind:
网络编程套接字(Socket通信)_第10张图片

listen:
网络编程套接字(Socket通信)_第11张图片
accept:
网络编程套接字(Socket通信)_第12张图片
connect:
网络编程套接字(Socket通信)_第13张图片

TCP单进程通信

实现一个简单的通信:client向服务器发送数据,服务器server收到后,给client一个应答

Makefile文件

1 FLAG=-std=c++11                                                                                                     
  2 
  3 .PHONY:all
  4 all:tcpClient tcpServer
  5 
  6 tcpClient:tcpClient.cc
  7   g++ -o $@ $^ $(FLAG) 
  8 tcpServer:tcpServer.cc
  9   g++ -o $@ $^ $(FLAG)
 10 
 11 .PHONY:clean
 12 clean:
 13   rm -f tcpClient tcpServer 
~

tcpServer.hpp文件

1 #ifndef __TCP_SERVER_H__                                                                                            
  2 #define __TCP_SERVER_H__ 
  3 
  4 
  5 #include<iostream>
  6 #include<string>
  7 #include<cstdlib>
  8 #include<unistd.h>
  9 #include<sys/types.h>
 10 #include<sys/socket.h>
 11 #include<netinet/in.h>
 12 #include<arpa/inet.h>
 13 #include<cstring>
 14 #define BACKLOG 5
 15 
 16 //***************************************************************
 17 //****************  单进程服务器  *******************************
 18 //***************************************************************
 19 class tcpServer 
 20 {
 21   private:
 22     int port;
 23     int lsock; //监听套接字
 24   public:
 25     tcpServer(int _port)
 26       :port(_port)
 27       ,lsock(-1)
 28   {
 29 
 30   }
 31     void initServer()
 32     {
 33       //创建套接字,创建更多的是文件方面的信息
 34       lsock=socket(AF_INET,SOCK_STREAM,0); //SOCK_STREAM:流式套接字
 35       if(lsock < 0) //文件描述符<0创建失败
 36       {
 37         std::cerr << "socker error" <<std::endl;
 38         exit(2);
 39       }
 40                                                                                                                     
 41       //填充套接字信息到用户层
 42       struct sockaddr_in local;
 43       local.sin_family=AF_INET;      //ipv4
 44       local.sin_port =htons(port);  //主机转网络序列
 45       local.sin_addr.s_addr=htonl(INADDR_ANY); //主机转网络
  46       
 47       //绑定将套接字填入到内核里,绑定就是将文件信息与网络信息进行绑定
 48       if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)
 49       {
 50         std::cerr << "bind error " <<std::endl;                                                                     
 51         exit(3);
 52       }
 53       
 54       //将套接字设置为监听状态
 55       //listen将套接字的状态设置为监听状态,所谓监听状态就是允许在任
 56       //何时刻客服端可以链接我 BACKLOG为全链接的长度
 57       if(listen(lsock,BACKLOG) < 0 )  //成功返回0,失败返回-1
 58       {
 59         std::cerr << "bind error" <<std::endl;
 60         exit(4);
 61       }
 62     }
 63     void service(int sock)
 64     {
 65       char buffer[1024];
 66       while(true)
 
67       {
 68         //recv send和read write相当
 69         //buffer从哪里读,sizeof(buffer)期望读多少,最后一个参数是选择是否非阻塞,0表示阻塞
 70         //返回值是读了多少字节
 71         size_t s = recv(sock,buffer,sizeof(buffer)-1,0);
 72         if(s > 0) //说明读取成功
 73         {
 74           buffer[s] = 0;
 75           std::cout<< "client# " << buffer <<std::endl;
 76 
 77           //回消息给客服端
 78           send(sock,buffer,strlen(buffer),0);
 79         }
 80       else if(s == 0)  //说明对端关闭了
 81         {
 82           std::cout << "client quit ..." << std::endl;
 83           break;                                                                                                    
 84         }
 85         else //s<0 读取失败 
 86         {
 87           std::cout << "recv client data error..." << std::endl;
 88           break;
 89         }
 90       }
 91       close(sock);
 92     }
 93     void start()
 94     {
 95       sockaddr_in endpoint;
 96       while(true)
 97       {
 98         socklen_t len = sizeof(endpoint);
 99         //accept第一个参数是套接字,最后两个参数就对方的ip和长度。返回值是一个文件描述符,失败返回-1
100         //这个返回值和socket返回值的区别是:负责拉客的是lsock,对客人服务的是accept的返回值sock,客人是链接
101         int sock = accept(lsock,(struct sockaddr*)&endpoint,&len);
102         
103         if(sock < 0)
104         {                                                                                                           
105           std::cerr << "accept error" << std::endl;
106           continue;
107         }
108         std::cout << " get a new link..." << std::endl;
109         //客人来了,要对他服务。对用户提供服务
110         service(sock);
111       }
112     }
113     ~tcpServer()
114     {
115 
116     }
117 };
118 
119 #endif                     

tcpServer.cc文件

 1 #include "tcpServer.hpp"                                                                                            
  2 
  3 static void Usage(std::string proc)
  4 {
  5   std::cout << "Usage: " << "prot" << std::endl;
  6   std::cout << '\t' << proc << " prot" <<std::endl;
  7 }
  8 
  9 int main(int argc,char* argv[])
 10 {
 11   if(argc != 2)
 12   {
 13     Usage(argv[0]);
 14     exit(1);
 15   }
 16 
 17   tcpServer* tp=new tcpServer(atoi(argv[1]));
 18   tp->initServer();
 19   tp->start();
 20   delete tp;
 21   return 0;
 22 }

tcpClient.hpp文件

 1 #ifndef __TCP_CLIENT_H__                                                                                            
  2 #define __TCP_CLIENT_H__ 
  3 
  4 #include<iostream>
  5 #include<string>
  6 #include<stdlib.h>
  7 #include<unistd.h>
  8 #include<sys/types.h>
  9 #include<sys/socket.h>
 10 #include<netinet/in.h>
 11 #include<cstring>
 12 #include<arpa/inet.h>
 13 
 14 class tcpClient
 15 {
 16   private:
 17     std::string svr_ip;  //服务器的ip
 18     int svr_port;   //服务器的端口号
 19     int sock;
 20   public:
 21     tcpClient(std::string _ip="127.0.0.1",int _port=8080) //127.0.0.1本地环回
 22         :svr_ip(_ip)
 23         ,svr_port(_port)
 24     {
 25 
 26     }
 27     void initClient()
 28     {
 29       //创建套接字   ipv4  ,流式套接字  阻塞  返回值是文件描述符
 30       sock = socket(AF_INET, SOCK_STREAM, 0);
 31       if(sock < 0 )
 32       {
 33         std::cerr << "socket error" <<std::endl;
 34         exit(2); //进程终止
 35       }
 36       
 37       //客服端不需要绑定,客服端在用户主机上跑。让OS去帮我们去绑定
 38       //客服端也不需要监听:监听的本质是为了让人随时随地来连我,客服端不需要别人连
 39       //客服端也不需要accept
 40       //tcp客服端要做的是链接服务器
 41                                                                                                                     
 42       //要链接的服务器信息
 43       struct sockaddr_in svr;
 44       svr.sin_family=AF_INET;
 45       svr.sin_port = htons(svr_port);
 46       svr.sin_addr.s_addr=inet_addr(svr_ip.c_str()); 
  47       
 48       //第一个参数,是套接字,connect后两个参数是要链接服务器的信息 ,成功返回0 ,失败返回-1
 49       if(connect(sock,(struct sockaddr*)&svr,sizeof(svr)) != 0)
 50       {
 51         std::cerr << "connect error" << std::endl;
 52       }
 53 
 54     }
 55 
 56     void start()
 57     {
 58       char msg[64];
 59 
 60       while(true)
 61       {
 62         std::cout << "Please Enter Message# ";
 63         fflush(stdout);                                                                                             
 64 
 65         //0是标准输入,从标准输入里读,也就是键盘,读到msg里,期望都sizeof(msg)-1个字节
 66         size_t s=read(0,msg,sizeof(msg)-1); //返回值是读了多少字节
 67         if(s>0)
 68         {
 69           msg[s-1]=0;
 70           //往sock里写
 71           send(sock,msg,strlen(msg),0);
 72          ssize_t ss= recv(sock,msg,sizeof(msg)-1,0);
 73          if(ss > 0)
 74          {
 75            msg[ss]=0;
 76            std::cout<< " server echo # " << msg <<std::endl;
 77          }
 78         }
 79       }
 80     }
 81     ~tcpClient()
 82     {
 83       close(sock);
 84     }
 85 };
 86 
 87 #endif                           

tcpClient.cc文件

 1 #include "tcpClient.hpp"
  2 
  3 void Usage(std::string proc)
  4 {
  5   std::cout<< "Usage: \n" << "\t";
  6   std::cout<< proc << "svr_ip svr_port" << std::endl;
  7 
  8 }
  9 
 10 int main(int argc,char* argv[])
 11 {  
 12     if(argc !=3)
 13     {
 14       Usage(argv[0]);
 15       exit(1);
 16     }
 17   tcpClient* tc =new tcpClient(argv[1],atoi(argv[2]));
 18   tc->initClient();
 19   tc->start();                                                                                                      
 20   delete tc;
 21   return 0;        
 22 }             

网络编程套接字(Socket通信)_第14张图片
效果演示:

网络编程套接字(Socket通信)_第15张图片

TCP多进程通信

Makefile文件

1 FLAG=-std=c++11                                                                                                     
  2 
  3 .PHONY:all
  4 all:tcpClient tcpServer
  5 
  6 tcpClient:tcpClient.cc
  7   g++ -o $@ $^ $(FLAG) 
  8 tcpServer:tcpServer.cc
  9   g++ -o $@ $^ $(FLAG)
 10 
 11 .PHONY:clean
 12 clean:
 13   rm -f tcpClient tcpServer 

tcpServer.hpp文件

1 #ifndef __TCP_SERVER_H__                                                                                            
  2 #define __TCP_SERVER_H__ 
  3 
  4 
  5 #include<iostream>
  6 #include<string>
  7 #include<cstdlib>
  8 #include<unistd.h>
  9 #include<sys/types.h>
 10 #include<sys/socket.h>
 11 #include<netinet/in.h>
 12 #include<arpa/inet.h>
 13 #include<cstring>
 14 #include<signal.h>
 15 
 16 #define BACKLOG 5
 17  
 18 // **********************************************
 19 // ************  多进程服务器  ******************
 20 // **********************************************
 21 class tcpServer 
 22 {
 23   private:
  24     int port;
 25     int lsock; //监听套接字
 26   public:
 27     tcpServer(int _port)
 28       :port(_port)
 29       ,lsock(-1)
 30   {
 31 
 32   }
 33     void initServer()
 34     {
 35       signal(SIGCHLD,SIG_IGN); //忽略子信号
 36       //创建套接字,创建更多的是文件方面的信息
 37       lsock=socket(AF_INET,SOCK_STREAM,0); //SOCK_STREAM:流式套接字
 38       if(lsock < 0) //文件描述符<0创建失败
 39       {
 40         std::cerr << "socker error" <<std::endl;                                                                    
 41         exit(2);
 42       }
 43 
 44       //填充套接字信息到用户层
 45       struct sockaddr_in local;
  46       local.sin_family=AF_INET;      //ipv4
 47       local.sin_port =htons(port);  //主机转网络序列
 48       local.sin_addr.s_addr=htonl(INADDR_ANY); //主机转网络
 49       
 50       //绑定将套接字填入到内核里,绑定就是将文件信息与网络信息进行绑定
 51       if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)                                                    
 52       {
 53         std::cerr << "bind error " <<std::endl;
 54         exit(3);
 55       }
 56       
 57       //将套接字设置为监听状态
 58       //listen将套接字的状态设置为监听状态,所谓监听状态就是允许在任
 59       //何时刻客服端可以链接我 BACKLOG为全链接的长度
 60       if(listen(lsock,BACKLOG) < 0 )  //成功返回0,失败返回-1
 61       {
 62         std::cerr << "bind error" <<std::endl;
 63         exit(4);
 64       }
 65     }
 66     void service(int sock)
 67     {
 68       char buffer[1024];
  69       while(true)
 70       {
 71         //recv send和read write相当
 72         //buffer从哪里读,sizeof(buffer)期望读多少,最后一个参数是选择是否非阻塞,0表示阻塞
 73         //返回值是读了多少字节
 74         size_t s = recv(sock,buffer,sizeof(buffer)-1,0);
 75         if(s > 0) //说明读取成功
 76         {
 77           buffer[s] = 0;
 78           std::cout<< "client# " << buffer <<std::endl;
 79 
 80           //回消息给客服端
 81           send(sock,buffer,strlen(buffer),0);
 82         }
 83       else if(s == 0)  //说明对端关闭了
 84         {
 85           std::cout << "client quit ..." << std::endl;                                                              
 86           break;
 87         }
 88         else //s<0 读取失败 
 89         {
 90           std::cout << "recv client data error..." << std::endl;
 91           break;
 92         }
 93       }
 94       close(sock);
 95     }
 96     void start()
 97     {
 98       sockaddr_in endpoint;
 99       while(true)
100       {
101         socklen_t len = sizeof(endpoint);
102         //accept第一个参数是套接字,最后两个参数就对方的ip和长度。返回值是一个文件描述符,失败返回-1
103         //这个返回值和socket返回值的区别是:负责拉客的是lsock,对客人服务的是accept的返回值sock,客人是链接
104         int sock = accept(lsock,(struct sockaddr*)&endpoint,&len);
105         
106         if(sock < 0)
107         {
108           std::cerr << "accept error" << std::endl;                                                                 
109           continue;
110         }
111         std::string cli_info = inet_ntoa(endpoint.sin_addr);//4字节ip转为点分10进制ip
112         cli_info += ":";
113         cli_info += std::to_string(ntohs(endpoint.sin_port)); //port转为字符串
114 
115         std::cout << " get a new link..." << cli_info << " sock: " << sock << std::endl;
116 
117         //让子进程去服务
118         pid_t id = fork();
119         if(id == 0)
120         {
121           //子进程关心的是sock,不关心lsock
122           close(lsock); //不影响父进程的lsock,可以不关闭,最好关闭
123           //客人来了,要对他服务。对用户提供服务
124           service(sock);
125           exit(0);
126         }
127 
128         //父进程应该关心lsock,父进程必须关闭sock,因为父进程的主要职责是不断
129         //获取链接,当父进程不断accept获取连接时,一定会带来一个后果,父进程获取的
130         //套接字被子进程拿去服务后,父进程就没用了,这个文件描述符还占着呢,导致父进程                              
131         //可用的文件描述符越来越少
132         close(sock); //不影响子进程的sock,因为大家用的是不同的文件描述符表
133       }
134     }
135     ~tcpServer()
136     {
137 
138     }
139 };
140 
141 #endif       

tcpServer.cc文件

1 #include "tcpServer.hpp"                                                                                            
  2 
  3 static void Usage(std::string proc)
  4 {
  5   std::cout << "Usage: " << "prot" << std::endl;
  6   std::cout << '\t' << proc << " prot" <<std::endl;
  7 }
  8 
  9 int main(int argc,char* argv[])
 10 {
 11   if(argc != 2)
 12   {
 13     Usage(argv[0]);
 14     exit(1);
 15   }
 16 
 17   tcpServer* tp=new tcpServer(atoi(argv[1]));
 18   tp->initServer();
 19   tp->start();
 20   delete tp;
 21   return 0;
 22 }

tcpClient.hpp文件

1 #ifndef __TCP_CLIENT_H__                                                                                            
  2 #define __TCP_CLIENT_H__ 
  3 
  4 #include<iostream>
  5 #include<string>
  6 #include<stdlib.h>
  7 #include<unistd.h>
  8 #include<sys/types.h>
  9 #include<sys/socket.h>
 10 #include<netinet/in.h> 
 11 #include<cstring>
 12 #include<arpa/inet.h>
 13 
 14 class tcpClient
 15 {
 16   private:
 17     std::string svr_ip;  //服务器的ip
 18     int svr_port;   //服务器的端口号
 19     int sock;
 20   public:
 21     tcpClient(std::string _ip="127.0.0.1",int _port=8080) //127.0.0.1本地环回
 22         :svr_ip(_ip)
 23         ,svr_port(_port)
 24     {
 25 
 26     }
 27     void initClient()
 28     {
 29       //创建套接字   ipv4  ,流式套接字  阻塞  返回值是文件描述符
 30       sock = socket(AF_INET, SOCK_STREAM, 0);
 31       if(sock < 0 )
 32       {
 33         std::cerr << "socket error" <<std::endl;
 34         exit(2); //进程终止
 35       }
 36       
 37       //客服端不需要绑定,客服端在用户主机上跑。让OS去帮我们去绑定
 38       //客服端也不需要监听:监听的本质是为了让人随时随地来连我,客服端不需要别人连
 39       //客服端也不需要accept                                                                                        
 40       //tcp客服端要做的是链接服务器
 41       
 42       //要链接的服务器信息
 43       struct sockaddr_in svr;
 44       svr.sin_family=AF_INET;
 45       svr.sin_port = htons(svr_port);
 46       svr.sin_addr.s_addr=inet_addr(svr_ip.c_str()); 
 47       
 48       //第一个参数,是套接字,connect后两个参数是要链接服务器的信息 ,成功返回0 ,失败返回-1
 49       if(connect(sock,(struct sockaddr*)&svr,sizeof(svr)) != 0)
 50       {
 51         std::cerr << "connect error" << std::endl;
 52       }
 53 
 54     }
 55 
 56     void start()
 57     {
 58       char msg[64];
 59 
 60       while(true)
 61       {                                                                                                             
 62         std::cout << "Please Enter Message# ";
 63         fflush(stdout); //刷新缓冲区
 64 
 65         //0是标准输入,从标准输入里读,也就是键盘,读到msg里,期望都sizeof(msg)-1个字节
 66         size_t s=read(0,msg,sizeof(msg)-1); //返回值是读了多少字节
  67         if(s>0)
 68         {
 69           msg[s-1]=0;
 70           //往sock里写
 71           send(sock,msg,strlen(msg),0);
 72          ssize_t ss= recv(sock,msg,sizeof(msg)-1,0);
 73          if(ss > 0)
 74          {
 75            msg[ss]=0;
 76            std::cout<< " server echo # " << msg <<std::endl;
 77          }
 78         }
 79       }
 80     }
 81     ~tcpClient()
 82     {
 83       close(sock);
 84     }
 85 };
 86 
 87 #endif                             

tcpClient.cc文件

1 #include "tcpServer.hpp"                                                                                            
  2 
  3 static void Usage(std::string proc)
  4 {
  5   std::cout << "Usage: " << "prot" << std::endl;
  6   std::cout << '\t' << proc << " prot" <<std::endl;
  7 }
  8 
  9 int main(int argc,char* argv[])
 10 {
 11   if(argc != 2)
 12   {
 13     Usage(argv[0]);
 14     exit(1);
 15   }
 16 
 17   tcpServer* tp=new tcpServer(atoi(argv[1]));
 18   tp->initServer();
 19   tp->start();
 20   delete tp;
 21   return 0;
 22 }

TCP多线程通信

Makefile文件

1 FLAG=-std=c++11 -lpthread                                                                                           
  2 
  3 .PHONY:all
  4 all:tcpClient tcpServer
  5 
  6 tcpClient:tcpClient.cc
  7   g++ -o $@ $^ $(FLAG) 
  8 tcpServer:tcpServer.cc
  9   g++ -o $@ $^ $(FLAG)
 10 
 11 .PHONY:clean
 12 clean:
 13   rm -f tcpClient tcpServer 
~

tcpServer.hpp文件

1 #ifndef __TCP_SERVER_H__
    2 #define __TCP_SERVER_H__ 
    3 
    4                                                                                                                   
    5 #include<iostream>
    6 #include<string>
    7 #include<cstdlib>
    8 #include<unistd.h>
    9 #include<sys/types.h>
   10 #include<sys/socket.h>
   11 #include<netinet/in.h>
   12 #include<arpa/inet.h>
   13 #include<cstring>
   14 #include<signal.h>
   15 #include<pthread.h>
   16 
   17 #define BACKLOG 5
   18 
   19 // **********************************************
   20 // ************  多线程服务器  ******************
   21 // **********************************************
   22 class tcpServer
   23 {
   24   private:
   25     int port;
   26     int lsock; //监听套接字
   27   public:
   28     tcpServer(int _port)
   29       :port(_port)
   30       ,lsock(-1)
   31   {
   32 
   33   }
   34     void initServer()
   35     {
   36       //signal(SIGCHLD,SIG_IGN); //忽略子信号
   37       //创建套接字,创建更多的是文件方面的信息
   38       lsock=socket(AF_INET,SOCK_STREAM,0); //SOCK_STREAM:流式套接字
   39       if(lsock < 0) //文件描述符<0创建失败                                                                        
   40       {
   41         std::cerr << "socker error" <<std::endl;
   42         exit(2);
   43       }
   44 
   45       //填充套接字信息到用户层
   46       struct sockaddr_in local;
   47       local.sin_family=AF_INET;      //ipv4
   48       local.sin_port =htons(port);  //主机转网络序列
   49       local.sin_addr.s_addr=htonl(INADDR_ANY); //主机转网络
   50       
   51       //绑定将套接字填入到内核里,绑定就是将文件信息与网络信息进行绑定
   52       if(bind(lsock,(struct sockaddr*)&local,sizeof(local)) < 0)
   53       {
   54         std::cerr << "bind error " <<std::endl;
   55         exit(3);
   56       }
   57       
   58       //将套接字设置为监听状态
   59       //listen将套接字的状态设置为监听状态,所谓监听状态就是允许在任
   60       //何时刻客服端可以链接我 BACKLOG为全链接的长度
   61       if(listen(lsock,BACKLOG) < 0 )  //成功返回0,失败返回-1                                                     
   62       {
   63         std::cerr << "bind error" <<std::endl;
   64         exit(4);
   65       }
   66     }
   67     static void service(int sock)
   68     {
   69       char buffer[1024];
   70       while(true)
   71       {
   72         //recv send和read write相当                                                                               
   73         //buffer从哪里读,sizeof(buffer)期望读多少,最后一个参数是选择是否非阻塞,0表示阻塞
   74         //返回值是读了多少字节
   75         size_t s = recv(sock,buffer,sizeof(buffer)-1,0);
   76         if(s > 0) //说明读取成功
   77         {
   78           buffer[s] = 0;
   79           std::cout<< "client# " << buffer <<std::endl;
   80 
   81           //回消息给客服端
   82           send(sock,buffer,strlen(buffer),0);
   83         }
   84       else if(s == 0)  //说明对端关闭了
   85         {
   86           std::cout << "client quit ..." << std::endl;
   87           break;
   88         }
   89         else //s<0 读取失败 
   
90         {
   91           std::cout << "recv client data error..." << std::endl;
   92           break;
   93         }
   94       }
   95       close(sock);
   96     }
   97     
   98     //让线程去提供服务
   99     static void* serviceRoutine(void* arg)
  100     {
  101       pthread_detach(pthread_self());  //线程分离,主线程不必再关心子线程
  102      
  103       std::cout << "create a new thread for IO" <<std::endl;
  104       int* p = (int*)arg;
  105       int sock = *p;                                                                                              
  106       service(sock);
  107       delete p;
  108 
109     }
110     void start()
  111     {
  112       sockaddr_in endpoint;
  113       while(true)                                                                                                 
  114       {
  115         socklen_t len = sizeof(endpoint);
  116         //accept第一个参数是套接字,最后两个参数就对方的ip和长度。返回值是一个文件描述符,失败返回-1
  117         //这个返回值和socket返回值的区别是:负责拉客的是lsock,对客人服务的是accept的返回值sock,客人是链接
  118         int sock = accept(lsock,(struct sockaddr*)&endpoint,&len);
  119         
  120         if(sock < 0)
  121         {
  122           std::cerr << "accept error" << std::endl;
  123           continue;
  124         }
  125         std::string cli_info = inet_ntoa(endpoint.sin_addr);//4字节ip转为点分10进制ip
  126         cli_info += ":";
  127         cli_info += std::to_string(ntohs(endpoint.sin_port)); //port转为字符串
  128 
  129         std::cout << " get a new link..." << cli_info << " sock: " << sock << std::endl;
  130 
  131         pthread_t tid;
  132         int *p = new int(sock);
  133         //创建线程
  134         //serviceRoutine线程要执行的函数
  135         pthread_create(&tid,nullptr,serviceRoutine,(void*)p);
  136       }
  137     }
  138     ~tcpServer()
  139     {
  140 
  141     }
  142 };
  143 
  144 #endif                               

tcpServer.cc文件

130 
  131         pthread_t tid;
  132         int *p = new int(sock);
  133         //创建线程
  134         //serviceRoutine线程要执行的函数
  135         pthread_create(&tid,nullptr,serviceRoutine,(void*)p);
  136       }
  137     }
  138     ~tcpServer()
  139     {
  140 
  141     }
  142 };
  143 
  144 #endif                               

tcpClient.hpp文件

1 #ifndef __TCP_CLIENT_H__                                                                                            
  2 #define __TCP_CLIENT_H__ 
  3 
  4 #include<iostream>
  5 #include<string>
  6 #include<stdlib.h>
  7 #include<unistd.h>
  8 #include<sys/types.h>
  9 #include<sys/socket.h>
 10 #include<netinet/in.h>
 11 #include<cstring>
 12 #include<arpa/inet.h>
 13 
 14 class tcpClient
 15 {
 16   private:
 17     std::string svr_ip;  //服务器的ip
 18     int svr_port;   //服务器的端口号
 19     int sock;
 20   public:
 21     tcpClient(std::string _ip="127.0.0.1",int _port=8080) //127.0.0.1本地环回
 22         :svr_ip(_ip)
 23         ,svr_port(_port)
 24     {
 25 
 26     }
 27     void initClient()
 28     {
 29       //创建套接字   ipv4  ,流式套接字  阻塞  返回值是文件描述符
 30       sock = socket(AF_INET, SOCK_STREAM, 0);
 31       if(sock < 0 )
 32       {
 33         std::cerr << "socket error" <<std::endl;
 34         exit(2); //进程终止
 35       }
 36       
 37       //客服端不需要绑定,客服端在用户主机上跑。让OS去帮我们去绑定
 38       //客服端也不需要监听:监听的本质是为了让人随时随地来连我,客服端不需要别人连
 39       //客服端也不需要accept
 40       //tcp客服端要做的是链接服务器                                                                                 
 41       
 42       //要链接的服务器信息
 43       struct sockaddr_in svr;
 44       svr.sin_family=AF_INET;
 45       svr.sin_port = htons(svr_port);
 46       svr.sin_addr.s_addr=inet_addr(svr_ip.c_str()); 
 47       
 48       //第一个参数,是套接字,connect后两个参数是要链接服务器的信息 ,成功返回0 ,失败返回-1
 49       if(connect(sock,(struct sockaddr*)&svr,sizeof(svr)) != 0)
 50       {
 51         std::cerr << "connect error" << std::endl;
 52       }
 53 
 54     }
 55 
 56     void start()
 57     {
 58       char msg[64];
 59 
 60       while(true)
 61       {
 62         std::cout << "Please Enter Message# ";                                                                      
 63         fflush(stdout); //刷新缓冲区
 64 
 65         //0是标准输入,从标准输入里读,也就是键盘,读到msg里,期望都sizeof(msg)-1个字节
 66         size_t s=read(0,msg,sizeof(msg)-1); //返回值是读了多少字节
 67         if(s>0)
 68         {
 69           msg[s-1]=0;
 70           //往sock里写
 71           send(sock,msg,strlen(msg),0);
 72          ssize_t ss= recv(sock,msg,sizeof(msg)-1,0);
 73          if(ss > 0)
 74          {
 75            msg[ss]=0;
 76            std::cout<< " server echo # " << msg <<std::endl;
 77          }
 78         }
 79       }
 80     }
 81     ~tcpClient()
 82     {
 83       close(sock);                                                                                                  
 84     }
 85 };
 86 
 87 #endif 

tcpClient.cc文件

 1 #include "tcpClient.hpp"                                                                                            
  2 
  3 void Usage(std::string proc)
  4 {
  5   std::cout<< "Usage: \n" << "\t";
  6   std::cout<< proc << "svr_ip svr_port" << std::endl;
  7 
  8 }
  9 
 10 int main(int argc,char* argv[])
 11 {
 12 
 13     if(argc !=3)
 14     {
 15       Usage(argv[0]);
 16       exit(1);
 17     }
 18   tcpClient* tc =new tcpClient(argv[1],atoi(argv[2]));
 19   tc->initClient();                                   
 20   tc->start();     
 21   delete tc;       
 22   return 0;   
 23 }           

tcp通信总结

1、单进程:不可使用
2、小型场景应用:
1、多进程版本:健壮性强(进程间具有独立性,一个进程出现问题不影响其它进程),比较吃资源,效率低下
2、多线程版本 :健壮性差,一个线程挂掉,可能导致整个进程挂掉,较吃资源,效率相对低下
大量客服端:系统会存在大量的执行流,切换(调度)可能成为效率低下的重要原因。

文件描述符与Socket的联系

网络编程套接字(Socket通信)_第16张图片
文件描述符与套接字的关系:
一个进程创建套接字的时候,本质是创建struct file,得到一个文件描述符,同时还要创建一个struct socket与struct file关联起来,让struct file 里的private_data指向struct socket ,同时让struct socket里的*file回指struct file ,struct socket里面会指向一个tcp_sock

你可能感兴趣的:(网络,udp,tcp/ip,UDP/TCP通信,socket网络编程)