socket网络编程

Posted 两片空白

tags:

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

目录

前言:

一.IP地址和端口号

        1.1 IP地址

        1.2 端口号

        1.3 端口号和进程PID区别

二.简单认识TCP协议和UDP协议

        2.1 介绍

三.网络字节序

        3.1 概念

        3.2 字节序转换函数

四.socket编程接口

        4.1 socket常见API

        4.2 struct sockaddr结构

        4.3 地址转换函数 

五.简单的UDP网络程序 

        5.1 介绍

        5.2 发送和接收数据使用到的函数

        5.3 代码

六.TCP协议通讯流程

七.简单编写TCP网络程序

        7.1介绍

        7.2 使用到的接口

        7.3 代码

 6.4 对比多线程和多进程版本


前言:

        网络通讯本质可以看成是进程间通讯,"同一份资源可以看成是网络"。就是本地的进程和远端服务器的进程进行通信。

一.IP地址和端口号

        1.1 IP地址

        IP地址是唯一标识一台主机的标志。

        在网络层添加到有效载荷(数据)的报头中含有两个IP地址。一个是源IP地址,一个是目的IP地址。

        源IP地址是一开始发送数据的主机,目的IP地址是最终收到数据的主机。

        1.2 端口号

        仅仅拥有IP地址我们还不能完成网络通讯。我们只是找到了哪台主机。一般数据是要发给进程进行处理的。

        端口号是传输层协议的内容:

  • 端口号是一个2字节16位的整数。
  • 端口号是用来标识进程的,告诉操作系统,当前这个数据要交给哪个进程处理。
  • IP地址 + 端口号可以唯一标识网络上的哪一台主机上的哪一个进程。
  • 一个端口号只能被一个进程占用,但是一个进程可以拥有多个端口号。

IP地址 + 端口号 就构成了套接字(socket)。

        在传输层协议的数据段(传输层加在有效载荷的报头)中,有两个端口号,分别是源端口号和目的端口号。

        源端口号是一开始发送数据的进程,目的端口号是最终收到数据的进程。

        1.3 端口号和进程PID区别

        之前我们学习进程的时候,我们知道在内核中有一个唯一标识进程的标识PID,但是在这里端口号也是唯一标识进程的。这两者有什么区别?

        首先端口号只是在网络里标识进程的,并且,每一个进程肯定都会有PID,OS管理进程的标识。但是,不是所有的进程都会有端口号。端口号只是参与网络通讯的进程才会有

        一个进程只能由一个PID,一个进程可以有多个端口号。

二.简单认识TCP协议和UDP协议

        2.1 介绍

        TCP(Transmision Constrol Protocol)协议和UDP(User Datagram Protocol)协议都是传输层的协议。

        TCP协议是有连接,会对数据进行可靠性检测,并且是面向字节流的协议。

        UDP协议是无连接不会对数据进行可靠性检测,是面向数据报的协议,UDP协议只是负责发送数据。

        这样看来TCP协议会比UDP协议复杂,并且效率会低一点。

三.网络字节序

        3.1 概念

        数据在内存中保存按字节会有大小端之分的。大端是高位保存在低地址处,小端是高位保存在高地址处。不仅内存保存数据会有高低地址之分,网络数据流也有大小端之分。

        如果两台主机保存数据的大小端模式不一样。发送数据主机是小端机,发送的数据是小端形式的,但是收到数据的主机是大端机,收到数据以大端形式保存。这样数据会以相反的形式保存,导致收到的数据和发送的数据不一致了。

        于是在发的送和接收数据时,有一个统一的规定,规定了网络发送和接收的字节序。

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发送
  • 接收主机把从网络接收到的字节依次保存到缓冲区中,也是按照从低到高的顺序保存
  • TCP/IP协议规定,网络数据流应采用大端字节序。
  • 不管这台主机是大端机还是小端机,都会按照大端字节序来发送和接收数据。
  • 如果当前主机是小端,先要将数据转成大端,再发送。

先发送的数据是内存低地址的数据的高权值位。

注意我们要发送的数据主要使IP地址和端口号。

        3.2 字节序转换函数

        为了使网络程序具有可移植性,使用同样的代码可以在大端机和小端机上都可以运行,可以调用下面函数,实现主机字节序和网络字节序的转换。

       #include <arpa/inet.h>
        //IP地址由主机字节序转成网络字节序
       uint32_t htonl(uint32_t hostlong);
        //端口号由主机字节序转成网络字节序
       uint16_t htons(uint16_t hostshort);
        //IP地址由网络字节序转成主机字节序
       uint32_t ntohl(uint32_t netlong);
        //端口号由网络字节序转成主机字节序
       uint16_t ntohs(uint16_t netshort);
  • 函数名,h表示host,n表示network,to表示转化成,l表示32位,s表示16位,IPv4的IP地址是32位的,端口号是16位的。
  • 如果主机的小端字节序,使用这些函数可以将参数转换成网络字节序。
  • 如果主机是大端字节序,这些函数不会做转换。

四.socket编程接口

        4.1 socket常见API

  • 创建网络相关的文件和资源
#include <sys/types.h> 
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

参数:

        domain:协议簇里的一个,一般AF_INET对应IPv4协议。

        type:类型常见两种,面向字节流(TCP)对应SOCK_STREAM。面向数据报对应SOCK_DGRAM(UDP)。

        protocol:协议,默认0,让系统选择。

返回值:

        socket的返回值是一个文件描述符。失败返回-1。系统也将网络描述成了一个文件。

        这里返回的是一个文件描述符,说明创建了一个文件,进程通过该文件来找到soket来对其进行控制。

//面向数据报
_sock = socket(AF_INET, SOCK_DGRAM, 0);

//面向流式
_sock = socket(AF_INET, SOCK_DGRAM, 0);

        如何理解socket和文件之间的关系:

查看源码:画一个简略图:

  • 绑定端口号和IP地址,将端口号和IP地址和创建的网络资源绑定
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数:

        sockfd:socket的返回值。

        addr :结构体里面保存了IP地址和端口号。

        addlen:addr大小

返回值:失败返回-1,成功返回0。

        4.2 struct sockaddr结构

        socket API是一层抽象的网络接口,适用于各种底层网络协议,不同的协议一样可以使用这些接口。但是这种网络协议的地址格式并不相同。

        我们使用最多的是struct sockaddr_in(对应IPv4)这个结构体。struct sockaddr_in结构是用来保存主机IP地址和端口号的。

        总结:struct sockaddr_in是以网络字节序的形式保存IP地址和端口号。

sockaddr_in结构

struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};
 
/* Internet address. */
struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};

socket API是一层抽象的网络接口,是如何实现适用于各种协议?

        一般接口函数都会有一个参数。struct sockaddr对应的上面通用的结构体。

        如果我们要使用跨网络通讯,使用struct sockaddr_in,我们只需要定义一个struct sockaddr_in对象,往里面放入对应端口号和IP地址。再强制转化成struct sockaddr*作为参数传给接口即可。

        但是我们传入的都是strcut sockaddr *,怎么知道我们传的是sockaddr_in还是sockaddr_un。三个结构体一开始都要有一个16位的地址类型。判断该数据。如果在sockaddr_in里,就是这个类型,并且执行对应的方法。如果在sockaddr_un里,就是这个类型,并且执行对应的方法。

初始化:

        但是在服务器端我们一般将绑定的IP设置成一个宏INADDR_ANY,这个宏的值是0。服务器端可能有多张网卡,服务器端绑定这个IP(INAADDR_ANY)可以接收服务器所有IP收到的数据。

        4.3 地址转换函数 

  • 将将字符串转成四字节IP地址,并且从主机字节序转成网络字节序。
in_addr_t inet_addr(const char *cp);

        参数:一个表示IP地址的字符串。

        主要用在IPv4协议中字符串表示的IP地址转成网络字节序的4字节IP地址。赋值struct sockaddr_in的IP地址时使用。

  • 将网络字节序的四字节IP地址转成网络序列,并且转成字符串形式的"点分十进制"形式。
char *inet_ntoa(struct in_addr in);

参数:struct sockaddr_in里的sin_addr。

std::string _ip = "127.0.0.1";

//用struct sockaddr_in保存ipv4的套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);//端口号,字节序转网络字节序
local.sin_addr.s_addr = inet_addr(_ip.c_str());//string装C语言字符串,在装整数,在转字节序

std::string cli;
cli += inet_ntoa(_ip.sin_addr);//先转成主机序列,转字符串,点分十进制形式

        关于inet_ntoa函数返回值是char*,很明显在函数内部自己申请了一块空间来保存ip结果,那是否需要释放手动释放空间呢?

         在man手册里介绍,inet_ntoa函数在静态区开辟了一块空间,将结果保存到了静态区,不需要我们手动释放。

#include<stdio.h>
#include<netinet/in.h>
#include<arpa/inet.h>
  
int main(){
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
  
    addr1.sin_addr.s_addr = 0x00000000;
    addr2.sin_addr.s_addr = 0xffffffff;
    char *p1 = inet_ntoa(addr1.sin_addr);
    char *p2 = inet_ntoa(addr2.sin_addr);
    printf("p1 : %s, p2 : %s\\n",p1,p2);
                                                                                                                                                           
    return 0;
}

输出:

        我们发现p1,p2的值相同。

        原因是:inet_ntoa把结果放到了自己内部的静态区,第二次又将结果放到了静态区,覆盖率上一次的值。inet_ntoa函数使用的是同一空间。

        这个结果证明当有多个线程调用inet_ntoa函数时,会出现值覆盖的情况。所以inet_ntoa不是一个线程安全函数。

        在多线程环境下,推荐使用inet_ntop函数,这个函数由调用者提供一个缓冲区保存结果,可以避免线程安全问题。

五.简单的UDP网络程序 

        5.1 介绍

        简单实现一个汉译英的功能。客户端输入一个中文,服务器向客户端发送对应中文翻译的英文。

        UDP协议是无连接,面向数据报的协议。

服务器端:

        首先要创建套接字,然后将IP地址和端口和和套接字进行绑定。然后就可以接收数据和发送数据。客户端需要知道服务器的ip地址和进程端口号,进行绑定。

客户端:

        首先也是需要创建套接字,客户端不需要将IP地址和端口和和套接字进行绑定,直接发送和接收数据。客户端需要知道服务器的ip地址和进程端口号。

为什么客户端不需要绑定,服务器需要绑定?

        首先为什么服务器需要绑定,因为服务器端进程跑起来一般是不会终止,除非出现bug或者更新。一般这个进程端口号是确定的,如果换来换去,每次客户端向服务器端发送数据时都还需要去重新确认服务器的IP和端口号,很麻烦。所以服务器端一般不会轻易修改进程端口号,进程和端口号时强相关的,所以需要绑定一个确定的端口号。

        为什么客户端不需要绑定,首先计算机中跑了很多进程,你并不知道哪些端口号被使用了。如果不同的客户端绑定了相同的端口号,会导致客户端无法启动。端口号是表示唯一进程的。

        客户端进程需要端口号的唯一,但是并不需要明确是哪一个端口号。

        系统会自动绑定端口号给进程,因为系统知道那些端口号被使用了。

        5.2 发送和接收数据使用到的函数

使用在无连接的协议

  •  接收数据
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);

参数:

        sockfd:创建的套接字

        buf:要接收数据的缓冲区

        len:缓冲区大小

        flag:接收方式,默认为0,阻塞等待接收。

        src_addr:要发送数据端的IP地址和端口号信息。

        addrlen:src_addr的大小。

返回值:

        接收成功返回实际接收数据的个数,失败返回-1。如果接收为0,说明发送端关闭了套接字。

  • 发送数据
#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);

参数:

        sockfd:创建的套接字

        buf:要发送的数据

        len:数据大小

        flag:发送方式,默认为0,阻塞等待接收。

        src_addr:要接收数据端的IP地址和端口号信息。

        addrlen:src_addr的大小。

返回值:

        接收成功返回实际发送数据的个数,失败返回-1。

        5.3 代码

服务器端:

#pragma once 
#include <iostream>
#include <string>
#include <string.h>
#include <map>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>


class udpServer{
  public:
    //udpServer(/*const char *ip = "127.0.0.1", */int port = 8080)
    udpServer(int port = 8080)
      :/*_ip(ip)*/
      _port(port)
      {}
    void InitServer(){
      //创建套接字
      _sock = socket(AF_INET, SOCK_DGRAM, 0);
      //用struct sockaddr_in保存ipv4的套接字
      struct sockaddr_in local;
      local.sin_family = AF_INET;
      local.sin_port = htons(_port);//端口号,字节序转网络字节序
      //local.sin_addr.s_addr = inet_addr(_ip.c_str());//string装C语言字符串,在装整数,在转字节序
      local.sin_addr.s_addr = INADDR_ANY;//不需要绑定固定的IP了
      //绑定,注意强转,将创建的套接字和文件绑定
      if(bind(_sock,(struct sockaddr *)&local, sizeof(local)) < 0){
        std::cerr<<"bind error"<<std::endl;     
        exit(1);
      }

      _dict.insert(std::make_pair("up","上"));
      _dict.insert(std::make_pair("down","下"));
      _dict.insert(std::make_pair("left","左"));
      _dict.insert(std::make_pair("right","右"));
    }

    void Start(){
      while(1){
        char buf[64];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        //从网络读
        ssize_t n=recvfrom(_sock, buf, sizeof(buf), 0, (struct sockaddr *)&peer, &len);
        if(n > 0){
          //获得客户端IP和port
          std::string cli;
          cli += inet_ntoa(peer.sin_addr);//先转成主机序列,转字符串,点分十进制形式
          cli += ':';
          char tmp[16];
          sprintf(tmp,"%d",ntohs(peer.sin_port));//转成主机序列,再装字符串
          cli +=tmp;
          buf[n]='\\0';
          std::cout<<cli.c_str()<<"#"<<buf<<std::endl;

          std::string s = "unkonw";
          if(_dict.find(buf) != _dict.end()){
            s=_dict[buf];
          }
          std::string str;
          //str += "Server send #";
          str += s;
          str += '\\0';
          //发送
          sendto(_sock, str.c_str(), str.size(), 0, (struct sockaddr *)&peer, len);

        }
        
      }
      
    }
    
    ~udpServer(){
      close( _sock);

    }

  
  private:
    //std::string _ip;//IP地址
    int _port;//端口号
    int _sock;//创建套接字返回的文件描述符
    std::map<std::string,std::string> _dict;//字典
};

#include"udpServer.hpp"

void  Direction(char *str){
  std::cout<<"Please enter"<<str<<" server_port"<<std::endl;
}
//命令行式输入端口号
int main(int argc, char *argv[]){
  if(argc != 2){
    Direction(argv[0]);
    exit(1);
  }
  udpServer *s = new udpServer(atoi(argv[1]));
  s->InitServer();
  s->Start();

  return 0;
}

客户端:

#pragma once 
#include <iostream>
#include <string>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>


class udpClient{
  public:
    //初始化的是服务器的套接字
    udpClient(const char *ip = "127.0.0.1", int port = 8080)
      :_ip(ip)
      ,_port(port)
      {}
    void InitClient(){
      //创建套接字
      _sock = socket(AF_INET, SOCK_DGRAM, 0);
      //不需要绑定,服务器是被动的,不会主动找客户端,当客户端打开时,服务器就能找到客户端了。

    }

    void Start(){
        struct sockaddr_in peer;
        peer.sin_family = AF_INET;
        peer.sin_port =htons(_port);
        peer.sin_addr.s_addr = inet_addr(_ip.c_str());
      while(1){
        //发送
        std::string msg;
        std::cout<<"please enter #";
        getline(std::cin,msg);
        if(msg == "quit"){
          break;
        }
        msg += '\\0';
        sendto(_sock, msg.c_str(), msg.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
        char temp[128];
        recvfrom(_sock, temp, sizeof(temp), 0, nullptr, nullptr);
        std::cout<<temp<<std::endl;
      }
      
    }
    
    ~udpClient(){

    }

  
  private:
    std::string _ip;//IP地址
    int _port;//端口号
    int _sock;//创建套接字返回的文件描述符

};

#include"udpClient.hpp"

void Direction(char str[]){
  std::cout<<"Please Enter #"<<str<<" server_ip server_port"<<std::endl;
}
//命令行式输入端口号和ip
int main(int argc, char *argv[]){
  if(argc != 3){
    Direction(argv[0]);
    exit(1);
  }
  udpClient *c =new udpClient(argv[1], atoi(argv[2]));
  c->InitClient();
  c->Start();

  return 0;
}

六.TCP协议通讯流程

服务器初始化:

  • 调用socket,创建网络相关资源,返回文件描述符
  • 调用bind,将端口号和ip地址和网络资源绑定。如果该端口被其它进程占用,bing会失败。
  • 调用listen,监听连接,为accept做准备。
  • 调用accept,阻塞等到客户端连接。

客户端建立连接:

客户端建立连接的过程,称为三次握手。

  • 调用socket,创建网络相关资源,返回文件描述符
  • 调用connect,向服务器发起连接请求。(第一次握手)
  • 服务器收到客户端连接请求SYN,会应答一个SYN-ACK段,表示同意连接(第二次握手)
  • 客户端收到SYN-ACK,会从connect段返回,同时应答一个ACK段。(第三次握手)

数据传输:

        TCP协议提供全双工的通信服务,在同一连接中,一端写数据的同时,另一端也可以写数据。相对的时半双工,同一连接,同一时刻,只能有一端写数据。

  • 服务器接到连接,accept后,立马调用recv函数,没有数据阻塞等待。
  • 客户端调用send,向服务器发送数据,发完,调用recv阻塞等待数据。
  • 服务器接到数据后,调用send,向客户端发送数据,后调用recv函数,
  • 如此循环

断开连接过程:

断开连接的过程称为四次挥手。

  • 如果客户端没有请求,就调用close关闭连接,客户端向服务器发送FIN段。(第一次挥手)
  • 服务器收到FIN后,回应一个ACK,同时recv返回0。(第二次挥手)
  • recv返回0后,知道客户端关闭了连接,服务器端也close关闭accept打开的文件,关闭连接。服务器会向客户端发送FIN。(第三次挥手)
  • 客户端收到FIN后,返回一个ACK给服务器。(第四次挥手)

七.简单编写TCP网络程序

        7.1介绍

        简单实现一个汉译英的功能。客户端输入一个中文,服务器向客户端发送对应中文翻译的英文。

        TCP协议式有连接面向字节流的协议。

服务器端:

        首先要创建套接字,然后将IP地址和端口和和套接字进行绑定。还需要将服务器端设置为监听状态,来监听连接请求,监听到连接请求后要接受连接请求。接收后就可以接收和发送数据了。

客户端:

        首先也是需要创建套接字,客户端不需要将IP地址和端口和和套接字进行绑定。然后去连接服务器。连接后就可以发送的接收数据了。

        7.2 使用到的接口

  • 监听连接
#include <sys/types.h>
#include <sys/socket.h>

int listen(int sockfd, int backlog);

参数:

        sockfd:哪个套接字处于监听状态

        backlog:允许要多少客户端处于连接等待状态,底层连接等待队列长度。一般不会太大(一般设为5),如果接收到更多连接请求就忽略。

返回值:

        成功返回0,失败返回-1。

  • 接收连接
#include <sys/types.h>
#include <sys/socket.h>

 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

        sockfd:哪个套接字接收连接

        addr:输出型参数,请求连接的端的ip和端口号。

        addrlen:addr大小

返回值:

        accept返回值是一个文件描述符,创建了一个与网络套接字等资源关联的文件。

        该文件描述符和socket创建的文件有什么不同?

        在TCP中,socket访问的文件描述符专注于获取连接的操作,accept返回的文件描述符专注于通讯(读写)的操作。

  • 客户端连接服务器
#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数:

        sockfd:想要连接的套接字

        addr:要连接服务器的ip和端口号

        addrlen:addr大小

返回值:

        成功返回0,失败返回-1。

  • 接收数据
#include <sys/types.h>
#include <sys/socket.h>

 ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数:

        sockfd:就收数据端的套接字

        buf:接收数据的缓存区

        len:缓存区大小

        flags:接收数据方式。默认0,阻塞接收

返回值:

        接收成功返回实际接收数据的个数,失败返回-1。如果接收为0,说明发送端关闭了套接字。

  • 发送数据
#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数:

        sockfd:发送数据端的套接字

        buf:要发送的数据

        len:发送数据的大小

        flags:发送数据方式。默认0,阻塞接收

返回值:

        发送成功返回实际发送数据的个数,失败返回-1。

        7.3 代码

        这里要注意几个问题:当客户端退出时,服务器端要知道,recv接收数据大小为0,需要关闭accept返回的文件描述符。

服务器端:

#pragma once 

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//#include <signal.h>
#include <pthread.h>

#define BOCKLOG 5
class tcpServer{
  private:
    int _port;
    int _lsock;

  public:
    tcpServer(int port = 8080)
      :_port(port)
       ,_lsock(-1)
    {}
    void initServer(){
      //创建套接字
      _lsock = socket(AF_INET, SOCK_STREAM, 0);
      if(_lsock < 0){
        std::cerr<<"socket error"<<std::endl;
        exit(1);
      }
      struct sockaddr_in local;
      local.sin_family = AF_INET;
      local.sin_port = htons(_port);//保存在内存中是主机字节序,要转成网络字节序
      local.sin_addr.s_addr = htons(INADDR_ANY);
      //绑定
      if(bind(_lsock,(struct sockaddr *)&local, sizeof(local))<0){
        std::cerr<<"bind error"<<std::endl;
        exit(2);
      }
      //设为监听状态
      if(listen(_lsock, BOCKLOG) < 0){
        std::cerr<<"listen error"<<std::endl;
        exit(1);
      }


    }
    static void Server(int sock){
      while(1){
        char bufer[64];
        ssize_t n = recv(sock, bufer, sizeof(bufer), 0); 
        if(n > 0){
          bufer[n] = 0;
          //读到数据
          std::cout<<"client #"<<bufer<<std::endl;

          //发送数据
          //建立连接了发送时不需要客户端ip和port
          send(sock, bufer, sizeof(bufer), 0);
        }
        else if(n == 0){
          //客户端退出,没写入数据,服务器读到0
          //退出服务,去连接客户端
          std::cout<<"client quit"<<std::endl;
          break;
        }
        else{
          //读取失败
          break;
        }
      }
      //关闭accept创建的文件
      close(sock);
    }
    static void *Service(void *arg){
      std::cout<<"pid :"<<getpid()<<std::endl;
      //线程分离,不用主线程等待
      pthread_detach(pthread_self());
      int sock = *(int *)arg;
      Server(sock);
      return (void *)0;

    }
    
    void start(){
      //signal(SIGCHLD, SIG_IGN);
      while(1){
        //接收连接
        struct sockaddr_in cli;
        socklen_t len = sizeof(cli);
        int sock = accept(_lsock, (struct sockaddr *)&cli, &len);
        if(sock < 0){
          std::cerr<<"accept error"<<std::endl;
        }
        std::string cli_infor = inet_ntoa(cli.sin_addr);
        cli_infor += ':';
        cli_infor += std::to_string(ntohs(cli.sin_port));

        std::cout<<"get a link...."<<"client :"<<cli_infor<<std::endl;
        //多线程
       //不能关闭_lsock和sock,线程共用一个 
        //防止主进程链接另外客户端修改sock值
        int *p = new int(sock);
        pthread_t tid;
        //Service参数传入sock地址,如果直接传sock地址,
        pthread_create(&tid, nullptr, Service, (void *)p);
        
        //多进程
        //pid_t id = fork();
        //if(id == 0){//子进程提供服务
        //  //提供服务
        //  //子进程退出会向父进程发送SIGCHLD信号
        //  //如果处理动作为忽略系统会自动释放子进程资源
        //  close(_lsock);//关闭_lsock,不使用,不影响父进程
        //  Server(sock);
        //  exit(0);

        //}
        //close(sock);//关闭sock,不使用,不影响子进程
        //父进程去链接进程,不需要等待
      }

    }
    ~tcpServer(){
      close(_lsock);
    }

};
#include "tcpServer.hpp"

void Notice(std::string str){
  std::cout<<str<<std::endl;
  std::cout<<"\\t"<<str<<" server_port"<<std::endl;

}
int main(int argc, char *argv[]){
  if(argc != 2){
    Notice(argv[0]);
    exit(1);
  }
  tcpServer *s = new tcpServer(atoi(argv[1]));
  s->initServer();
  s->start();

  delete s;

  return 0;
}

客户端:

#pragma once 

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class tcpClient{
  private:
    std::string _ip;
    int _port;
    int _sock;
  public:
    tcpClient(std::string ip = "127.0.0.1", int port = 8080)
      :_ip(ip)
      ,_port(port)
      ,_sock(-1)
      {}
    void initClient(){
      _sock = socket(AF_INET,SOCK_STREAM,0);
      if(_sock < 0){
        std::cout<<"socket error"<<std::endl;
        exit(1);
      }
      //客户端不需要绑定,监听和接收连接
      //而是要去连接服务器
      //保存服务器的ip和port
      struct sockaddr_in sev;
      sev.sin_family = AF_INET;
      sev.sin_port = htons(_port);
      sev.sin_addr.s_addr = inet_addr(_ip.c_str());
      if(connect(_sock, (struct sockaddr *)&sev,sizeof(sev)) < 0){
        std::cerr <<"connect error"<<std::endl;
        exit(2);
      }
    
    }
    void start(){
      while(1){
        std::cout<<"Please Enter #";
        fflush(stdout);
        char tmp[64];
        size_t n =read(0, tmp, sizeof(tmp));
        //去掉/n
        if(n > 0){
          tmp[n-1] = 0;
          send(_sock, tmp, strlen(tmp),0);
          char buf[128];
          recv(_sock, buf, sizeof(buf), 0);
          std::cout<<"server #"<<buf<<std::endl;
        }
      }
    }
    ~tcpClient(){
      close(_sock);
    }
};

#include "tcpClient.hpp"

void Notice(std::string str){
  std::cout<<"Notice"<<std::endl;
  std::cout<<"\\t"<<str<<" sev_ip sev_port"<<std::endl;
}

int main(int argc, char *argv[]){
  if(argc !=3){
    Notice(argv[0]);
    exit(1);
  }
  tcpClient *c = new tcpClient(argv[1], atoi(argv[2]));
  c->initClient();
  c->start();
  delete c;

  return 0;
}

 6.4 对比多线程和多进程版本

        多线程:缺点:鲁棒性不好,一个线程异常退出,进程退出。

                        优点:占用资源较少,效率较好

        多进程:缺点:占用资源多,效率低,来一个客户端,创建一个进程。

                        有点:鲁棒性好,进程独立性。

但是当客户请求过多时,进程或者线程的调度就成了主要的矛盾。

        我们可以使用线程池,线程池一定程度上确定了线程的数量,不会有大量的线程。在一段时间统一创建一批线程,不需要在用户请求时创建。

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

VSCode自定义代码片段——JS中的面向对象编程

VSCode自定义代码片段9——JS中的面向对象编程

VSCode自定义代码片段14——Vue的axios网络请求封装

VSCode自定义代码片段14——Vue的axios网络请求封装

VSCode自定义代码片段14——Vue的axios网络请求封装

Python--网络编程-----socket代码实例