Linux_网络---TCP(1)

一. 主机字节序和网络字节序

32位机器CPU一次至少装载4字节, 这4字节在内存中的排列顺序就是字节序

字节序分为大端字节序: 低地址存高位

                   小端字节序:低地址存低位

利用union验证本机的字节序:

int main(){
  union {
    char a;
    int  b;
  } test;

  test.b = 1;

  if (test.a == 0) {
    printf("big endian\n");  //大端字节序
  }
  else if (test.a == 1){
    printf("little endian\n"); //小端字节序
  
  }
  return 0;
}

原理: 

现代PC大多采用小端字节序, 因此小端字节序又被成为主机字节序

规定网络字节序为大端字节序, 所有主机收发数据时要转换为大端字节序

// Linux 提供 4 个函数完成字节序转换

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);

// n net代表网络, h host代表主机
// l long 32位, 用于 ip地址转换
// s short 16位 用于 端口转换

  二. socket地址表示

   通用socket

struct sockaddr
{
    sa_family_t sa_family;  // 地址族类型
    char sa_data[14];       // 存放socket地址值
}

专用socket 

struct sockaddr_in
{
    sa_family_t sin_family; // 地址族
    uint16_t sin_port;      // 端口号
    struct in_addr sin_addr;// Ipv4 地址结构体
}

struct in_addr
{
    uint32_t s_addr;        // Ipv4 地址
}

写代码用sockaddr_in, 类型转换为 sockaddr

三.  ip地址转换函数

通常使用ip地址用点分十进制表示, 在编写代码的时候, 需要把ip转换为32位整数

#include <arpa/inet.h>

in_addr_t inet_addr (const char* strptr);

int inet_aton(const char *cp, struct in_addr *inp);

char *inet_ntoa(struct in_addr in);

四. TCP编程流程

TCP协议: 有连接, 可靠, 面向字节流

1. 创建 socket                      int socket(int domain, int type, int protocol);

2. 绑定地址信息(服务端)       int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 

3. 监听socket()服务端)         int listen(int sockfd, int backlog);
    backlog决定了内核中已完成连接队列的最大结点数

4. 接受连接(服务端)              int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

    调用accept从listen监听队列中接受一个连接, accept成功返回一个新的连接socket, 可通过新socket来与请求连接的

    客户端通信

5. 发起连接(客户端)               int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    客户端调用connect主动发起连接

6. 关闭连接                             int close(int fd);

    close 并非立即关闭一个连接, 而是将 fd 的引用计数 -1, 只有当 fd 为 0 时, 才真正关闭连接

    在多进程程序中, 子进程拷贝了父进程地址空间, 只有父子进程都close了socket, 才能关闭连接

    如果一定要立即终止连接, 使用

    int shutdown(int sockfd, int how);

    how: SHUT_RD 关闭读, SHUT_WR 关闭写, SHUT_RDWR 关闭读写

TCP数据读写 :

    对文件的读写操作 read和write同样适用于socket, socket编程接口也提供了专门用于socket数据读写的系统调用

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

        recv返回 - 1 出错, 返回大于0 表示实际读取的字节数, 返回 0 表示对方已经断开连接

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

        sned返回实际写入的字节长度

        flags 一般置 0 , MSG_OOB 发送或接受紧急数据, MSG_PEEK 窥探读缓存中的数据, 此次操作不会删除数据

如何判断连接已经断开:

原理:

tcp的连接管理中, 内建有保活机制: 当长时间 没有数据往来时, 每隔一段时间都会向对方发送一个保活探测包, 要求对方回复

当多次发送的保活探测包都没有响应, 则认为连接断开

编写代码:

连接断开, recv 返回为 0 ; send会触发异常SIGPIPE(导致进程退出)

五. 封装TCP常用操作 

class TcpSocket{
public:
  TcpSocket():_fd(-1){}
  ~TcpSocket(){
  }
public:
  // 创建套接字
  bool Socket(){
    _fd = socket(AF_INET, SOCK_STREAM, 0);
    if (_fd == -1){
      cout << "Socket err" << endl;
      return false;
    }
    return true;
  }
  //关闭套接字
  bool Close() {
    close(_fd);
    cout << "close fd: " << _fd << endl;
    return true;
  }
  // 绑定 ip  port
  bool Bind(string& ip, uint16_t port){
    sockaddr_in bindAddr;
    bindAddr.sin_family = AF_INET;
    bindAddr.sin_port = htons(port);
    bindAddr.sin_addr.s_addr = inet_addr(ip.c_str());
    if (bind(_fd, (sockaddr*)&bindAddr, sizeof bindAddr) == -1){
      cout << "Bind err" << endl;
      return false;
    }
    return true;
  }
  
  // 监听套接字
  bool Listen(int num){
    if (listen(_fd, num) == -1){
      cout << "listen err" << endl;
      return false;
    }
    return true;
  }
	
  // 服务器等待连接
  bool Accept(TcpSocket& newSock, string* ip = nullptr, uint16_t* port = nullptr){
    sockaddr_in peerAddr;
    socklen_t sockLen = sizeof peerAddr;
    int newFd = accept(_fd, (sockaddr*)&peerAddr, &sockLen);
    if (newFd < 0){
      cout << "accept err" << endl;
      return false;
    }
    newSock.SetFd(newFd);
    
    if (ip != nullptr){
        *ip = inet_ntoa(peerAddr.sin_addr);
    }

    if (port != nullptr) {
      *port = ntohs(peerAddr.sin_port);
    }

    return true;
  }

  bool Connect(string& ip, uint16_t port){  // 客户端连接
    sockaddr_in servAddr;
    servAddr.sin_family = AF_INET;
    servAddr.sin_port = htons(port);
    servAddr.sin_addr.s_addr = inet_addr(ip.c_str());
    if (connect(_fd, (sockaddr*)&servAddr, sizeof servAddr) == -1){
      cout << "Connect() error" << endl;
      return false;
    }

    return true;
  }

  bool Recv(string& msg){       //接受
    char buf[4096] = {0};
    //这里有问题: tcp流无数据边界, 不一定会一次接收完
    int recvSize = recv(_fd, buf, 4096, 0);
    if (recvSize < 0){
      cout << "recv() err" << endl;
      return false;
    }

    if (recvSize == 0){
      return false;
    }

    msg.assign(buf, recvSize);
    return true;
  }

  bool Send(string& msg){     // 发送
    int ret = send(_fd, msg.c_str(), msg.size(), 0);
    
    if (ret < 0){
      cout << "send() err" << endl;
      return false;
    }

    return true;
  }

public:
  void SetFd(int fd){
    _fd = fd;
  }

  int GetFd(){
    return _fd;  // socket 文件描述符
  }
private:
  int _fd;
};

使用封装的接口实现简单的回声客户端 和 回声服务器(单对单): 

服务端:

int main(){
  TcpSocket tcp;

  tcp.Socket();
  string ip = "192.168.30.145";
  tcp.Bind(ip, 14396);
  tcp.Listen(5);

  // 服务器一直运行, 等待客户端连接
  while(1) {
    TcpSocket newSock;
    string clntIp;
    uint16_t clntPort;
	// 没有客户端连接, 将一直在这里阻塞
    tcp.Accept(newSock, &clntIp, &clntPort);
    cout << "Connect: (" << clntIp << "--" << clntPort << ")" << endl;
	
	// 持续与连接的客户端收发信息
    for(;;){
      string msg;
      if (!newSock.Recv(msg)){
        newSock.Close();
        break;
      }
      cout << "recv: " << msg << endl;

      newSock.Send(msg);
    }


  }

  tcp.Close();
  return 0;
}

客户端: 

int main(){
  TcpSocket tcp;
  tcp.Socket();
  string ip = "192.168.30.145";
  tcp.Connect(ip, 14396); // 连接设置ip 端口的客户端
  
  // 循环收发消息
  while(1) {
    string msg;
    getline(cin, msg);
    tcp.Send(msg);

    string resp;
    tcp.Recv(resp);
    cout << "recv msg: " << msg << endl;
  }

  tcp.Close();
  return 0;
}

同一时刻, 只能有一个客户端与服务器通信: 

 

如果要让服务器端处理多个客户端连接请求:可以使用多线程或多进程, 父进程(主线程) 处理连接请求, 

与客户端的通信由子进程(子线程)实现. 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值