Socket网络编程学习笔记

MirrorYuChen
MirrorYuChen
发布于 2024-11-12 / 7 阅读
0
0

Socket网络编程学习笔记

0.Socket简介

数据链路层、网络层、传输层协议是在内核中实现的。因此操作系统需要实现一组系统调用,使得应用程序能访问这些协议提供的服务。实现这组系统调用的API主要有两套:socket和XTI,其中,XTI现在基本不再使用。
由socket定义的这一组API提供如下两个功能:

  • (1) 将应用程序数据从用户缓冲区中复制到TCP/UDP内核发送缓冲区,以交付内核来发送数据,或是从内核TCP/UDP接收缓冲区来复制数据到用户缓冲区,以读取数据;
  • (2) 应用程序可以通过它们来修改内核中各层协议的某些头部信息或其他数据结构,从而精细地控制底层通信地行为。

1.IP和端口

IP地址 端口
10.0.0.204 1234
  • IP地址:用于唯一标识你的网络设备;
  • 端口:用于区分主机上的不同应用;

2.TCP和UDP

2.1 TCP

Transimission Control Protocol传输控制协议,有两个特点:

  • (1) TCP协议是可靠的,底层会自动检测并回传丢失的数据包,因此对于调用者而言,发送的数据对方一定会接收到;
  • (2) 发送和接受到数据顺序是完全一致的。

TCP是基于“数据流”的协议,需要注意,TCP要求收发数据的双方扮演不同角色(服务器和客户端),服务器会被动等待连接,它不会主动发起请求。

2.2 UDP

User Datagram Protocol用户报文协议,UDP如它的名字,以“报文”为单位来收发数据。并且UDP不会自动回传丢失数据包,因此不保证数据一定能被对方接受到,正是由于缺少这些检查,UDP通常具有更低延迟并占用更少系统资源,它也更适合像视频语音通话这种实时性要求较好的应用。

3.Socket网络编程流程

socket

3.1 服务端 Server.cc文件

/*
 * @Author: chenjingyu
 * @Date: 2024-09-03 09:57:33
 * @Contact: 2458006466@qq.com
 * @Description: Server
 */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <stdio.h>
#include <unistd.h>
#include "Logger.h"


int main(int argc, char *argv[]) {
  /**
   * 第一个参数:IP类型,AF_INET表示IPv4,AF_INET6表示IPv6 
   * 第二个参数:数据传输类型,SOCK_STREAM表示流格式,多用于TCP,SOCK_DGRAM表示数据报格式,多用于UDP
   * 第三个参数:协议,0表示由前面两个参数自动推导协议类型,IPPROTO_TCP和IPPROTO_UDP分别表示TCP和UDP
   **/
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  CHECK_NE(sockfd, -1) << "socket create error.";

  /**
   * 对于客户端,服务端存在的唯一标识为IP地址+端口 
   * 这时,需要将这个socket绑定到一个IP地址和端口上
   **/
  struct sockaddr_in serv_addr;
  bzero(&serv_addr, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = inet_addr("0.0.0.0");
  serv_addr.sin_port = htons(8080);

  // 将socket地址与文件描述符绑定
  int ret = bind(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr));
  CHECK_NE(ret, -1) << "socket bind error.";

  /**
   * 使用listen监听端口
   * 第一个参数:待监听socket的文件描述符
   * 第二个参数:最大监听队列长度,系统建议最大值SOMAXCONN被定义为128
   **/
  ret = listen(sockfd, SOMAXCONN);
  CHECK_NE(ret, -1) << "socket listen error.";

  /**
   * 使用accept接收一个客户端连接,接收连接时,要保存客户端的socket地址信息
   **/
  struct sockaddr_in clnt_addr;
  socklen_t clnt_addr_len = sizeof(clnt_addr);
  bzero(&clnt_addr, sizeof(clnt_addr));
  int clnt_sockfd = accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len);
  LogInfo("new client fd %d! IP: %s Port: %d.\n", clnt_sockfd, inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port));
  
  /**
   * 使用read和write进行网络接口数据读写操作
   **/
  while (true) {
    char buf[1024];
    bzero(buf, sizeof(buf));
    ssize_t read_bytes = read(clnt_sockfd, buf, sizeof(buf));
    if (read_bytes > 0) {
      LogInfo("Message from client fd %d: %s", clnt_sockfd, buf);
      write(clnt_sockfd, buf, sizeof(buf));
    } else if (read_bytes == 0) {
      // 标识EOF
      LogInfo("Client fd %d disconnected.", clnt_sockfd);
      close(clnt_sockfd);
      break;
    } else if (read_bytes == -1) {
      // 有错误发生
      close(clnt_sockfd);
      LogError("socket read error.");
    }
  }
  
  return 0;
}

3.2 客户端 Client.cc文件

/*
 * @Author: chenjingyu
 * @Date: 2024-09-03 10:25:33
 * @Contact: 2458006466@qq.com
 * @Description: Client
 */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <stdio.h>
#include "Logger.h"

int main(int argc, char *argv[]) {
  // 1.创建socket
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  CHECK_NE(sockfd, -1) << "socket create error.";

  // 2.创建server的address
  struct sockaddr_in serv_addr;
  bzero(&serv_addr, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
  serv_addr.sin_port = htons(8080);

  // 3.连接server的address
  int ret = connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr));
  CHECK_NE(ret, -1) << "socket connect error.";

  while (true) {
    char buf[1024];
    bzero(&buf, sizeof(buf));

    scanf("%s", buf);
    ssize_t write_bytes = write(sockfd, buf, sizeof(buf));

    if (write_bytes == -1) {
      LogError("socket already diconnected, can't write anymore!");
      break;
    }

    bzero(&buf, sizeof(buf));
    ssize_t read_bytes = read(sockfd, buf, sizeof(buf));
    if (read_bytes > 0) {
      LogInfo("Message from server: %s.", buf);
    } else if (read_bytes == 0) {
      close(sockfd);
      LogInfo("Server socket disconnected!");
      break;
    } else if (read_bytes == -1) {
      close(sockfd);
      LogError("Socket read error.");
    }
  }

  return 0;
}

3.3 封装的 Logger文件

  • 头文件 Logger.h封装
/*
 * @Author: chenjingyu
 * @Date: 2024-09-03 11:31:51
 * @Contact: 2458006466@qq.com
 * @Description: Logger
 */
#pragma once
#include <sstream>
#include <iostream>

#ifdef _WIN32
#define FILENAME                                                           \
  (strrchr(__FILE__, '\\') ? (strrchr(__FILE__, '\\') + 1) : __FILE__)
#else
#define FILENAME                                                           \
  (strrchr(__FILE__, '/') ? (strrchr(__FILE__, '/') + 1) : __FILE__)
#endif

namespace mirror {
class LogStreamImpl {
public:
  LogStreamImpl(int level, const char *file, int line, bool abort_flag = false);
  ~LogStreamImpl();

  std::ostream &stream() { return log_stream_; }

private:
  int level_;
  std::string file_;
  int line_;
  bool abort_flag_;
  std::stringstream log_stream_;
};

void LogPrintfImpl(int level, const char *fmt, ...);

} // namespace mirror

enum LogLevel {
  LDebug = 0,
  LInfo = 1,
  LError = 2,
  LFatal = 3
};

// clang-format on
#define CHECK(x)                                                                                        \
  if (!(x))                                                                                                        \
  mirror::LogStreamImpl(3, FILENAME, __LINE__, true).stream()                  \
      << "Check failed: " #x << ": " // NOLINT(*)

#define CHECK_BINARY_IMPL(x, cmp, y) CHECK(x cmp y) << x << #cmp << y << " "
#define CHECK_EQ(x, y) CHECK_BINARY_IMPL(x, ==, y)
#define CHECK_NE(x, y) CHECK_BINARY_IMPL(x, !=, y)
#define CHECK_LT(x, y) CHECK_BINARY_IMPL(x, <, y)
#define CHECK_LE(x, y) CHECK_BINARY_IMPL(x, <=, y)
#define CHECK_GT(x, y) CHECK_BINARY_IMPL(x, >, y)
#define CHECK_GE(x, y) CHECK_BINARY_IMPL(x, >=, y)
// clang-format off

#define LogStream(LEVEL)                                                       \
  mirror::LogStreamImpl(LEVEL, FILENAME, __LINE__).stream()

#define LogPrintf(LEVEL, fmt, ...)                                             \
  mirror::LogPrintfImpl(LEVEL, "[%s: %d] " fmt, FILENAME, __LINE__,            \
                        ##__VA_ARGS__)

#define LogDebug(fmt, ...) LogPrintf(LDebug, fmt, ##__VA_ARGS__)
#define LogInfo(fmt, ...) LogPrintf(LInfo, fmt, ##__VA_ARGS__)
#define LogError(fmt, ...) LogPrintf(LError, fmt, ##__VA_ARGS__)
#define LogFatal(fmt, ...) LogPrintf(LFatal, fmt, ##__VA_ARGS__)
  • 实现文件 Logger.cc
/*
 * @Author: chenjingyu
 * @Date: 2024-09-03 11:40:48
 * @Contact: 2458006466@qq.com
 * @Description: Logger
 */
#include "Logger.h"
#include <stdarg.h>
#include <string>
#include <stdlib.h>
#include <stdio.h>

namespace mirror {
LogStreamImpl::LogStreamImpl(int level, const char *file, int line, bool abort_flag)
    : level_(level), file_(file), line_(line), abort_flag_(abort_flag) {
  log_stream_ << "[" << file_ << ": " << line_ << "] ";
}

LogStreamImpl::~LogStreamImpl() {
  switch (level_) {
    case LDebug:
      std::cout << "[DEBUG]";
      break;
    case LInfo:
      std::cout << "[INFO]";
      break;
    case LError:
      std::cout << "[ERROR]";
      break;
    case LFatal:
      std::cout << "[FATAL]";
      break;
    default:
      std::cout << "[DEBUG]";
      break;
  }
  std::cout << log_stream_.str() << std::endl;
  if (abort_flag_) {
    abort();
  }
}

static std::string StringPrintf(const char *format, va_list args) {
  std::string output;
  // 1.try a small buffer and hope it fits
  char space[128];
  va_list args_backup;
  va_copy(args_backup, args);
  int bytes = vsnprintf(space, sizeof(space), format, args_backup);
  va_end(args_backup);

  if ((bytes >= 0) &&
      (static_cast<size_t>(bytes) < sizeof(space))) {
    output.append(space, bytes);
    return output;
  }

  // 2.Repeatedly increase buffer size until it fits.
  int length = sizeof(space);
  while (true) {
    if (bytes < 0) {
      length *= 2;
    } else {
      length = bytes + 1;
    }
    char *buf = new char[length];
    // 2.1.Restore the va_list before we use it again
    va_copy(args_backup, args);
    bytes = vsnprintf(buf, length, format, args_backup);
    va_end(args_backup);

    if ((bytes >= 0) && (bytes < length)) {
      output.append(buf, bytes);
      delete[] buf;
      break;
    } else {
      delete[] buf;
    }  
  }
  return output;
}

void LogPrintfImpl(int level, const char *fmt, ...) {
  va_list args;
  va_start(args, fmt);
  std::string msg = StringPrintf(fmt, args);
  va_end(args);
  switch (level) {
    case LDebug:
      std::cout << "[DEBUG]";
      break;
    case LInfo:
      std::cout << "[INFO]";
      break;
    case LError:
      std::cout << "[ERROR]";
      break;
    case LFatal:
      std::cout << "[FATAL]";
      break;
    default:
      std::cout << "[DEBUG]";
      break;
  }
  std::cout << msg << std::endl;
}
} // namespace mirror

3.4 Makefile脚本文件

server:
	g++ Server.cc Logger.cc -o Server && g++ Client.cc Logger.cc -o Client

clean:
	rm Server && rm Client

3.5 编译运行

  • (1) 编译
>> make clean && make
  • (2) 运行Server
>> ./Server
  • (3) 运行Client
>> ./Client

最终效果如下:
Server_result

4.服务器上部署

4.1 购买一台云服务器

  • 我买的是腾讯云的服务器,具体可以参考前面搭建Halo博客系统的流程;

4.2 代码上传并编译

  • 编译流程和前面差不多

4.3 开放端口

  • 具体参考前面搭建Halo博客系统的流程,不再赘述;

4.4 防火墙设置

>> firewall-cmd --permanent --add-port=8080/tcp
You're performing an operation over default zone ('public'),
but your connections/interfaces are in zone 'docker' (see --get-active-zones)
You most likely need to use --zone=docker option.

success
>> firewall-cmd --reload
success

4.5 确认端口配置

  • 原始状态
>> netstat -an | grep 8080
unix  3      [ ]         STREAM     CONNECTED     58080 
  • 启动server后
>> netstat -an | grep 8080
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN   
unix  3      [ ]         STREAM     CONNECTED     58080  

4.6 使用telnet工具测试

>> telnet [IP地址] 8080
Trying [IP地址]...
Connected to [IP地址].
Escape character is '^]'.
Hello,world!
Hello,world!
你好呀!
你好呀!
你是傻逼?
你是傻逼?
大噶好,我是渣渣辉!
大噶好,我是渣渣辉!
  • 退出telnet
>> ctrl + ]
telnet> quit
Connection closed.

5.参考资料


评论