理解网络编程和套接字

理解网络编程和套接字

网络编程就是编写程序让两个计算机通过网络进行数据交互,不需要关注计算机之间是通过什么方式连接,也不需要关注传输数据的软件是什么,关注的是如何将让两个计算机建立连接以及怎么传输数据。

常见的网络编程有两种套接字:TCP 套接字和 UDP 套接字。下面通过一个简单的 TCP 套接字程序理解 tcp 服务器端和客户端的编程步骤。

TCP 服务器端基本流程

tcp 服务器端在 Linux 中一般有四步,要是再 Windows 中则要多几步,多的这几步仅限对库的使用。先看 Linux 和 Windows 两个共有的步骤:

  1. 创建套接字 ——> 可以简单的理解为买了一个手机,没有手机无法通信
  • Linux 中的接口函数
#include <sys/types.h>
#include <sys/socket.h>

// 成功返回文件描述符,失败返回 -1
int socket(int domain, int type, int protocol);
  • Windows 中的接口函数
 #include <winsock2.h>

 // 成功返回套接字,失败返回 INVALID_SOCKET
SOCKET socket(int af, int type, int protocol);
  1. 给套接字绑定地址信息 ——> 获取手机号需要绑定你的身份信息
  • Linux 中的接口函数
#include <sys/types.h>
#include <sys/socket.h>

// 成功返回 0,失败返回 -1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • Windows 中的接口函数
#include <winsock2.h>

// 成功返回 0,失败返回 SOCKET_ERROR
int bind(SOCKET s, const struct sockaddr *addr, socklen_t addrlen);
  1. 将套接字设置为接收连接的状态 ——> 手机中装上手机卡后,需要确保手机处于开机状态并且有信号,别人才可以打电话进来
  • Linux 中的接口函数
#include <sys/types.h>
#include <sys/socket.h>

// 成功返回 0,失败返回 -1
int listen(int sockfd, int backlog);
  • Windows 中的接口函数
#include <winsock2.h>

// 成功返回 0,失败返回 SOCKET_ERROR
int listen(SOCKET s, int backlog);
  1. 接收连接请求 ——> 按下接听按钮
  • Linux 中的接口函数
#include <sys/types.h>
#include <sys/socket.h>

// 成功返回非负整数,这个整数是客户端的文件描述符,失败返回 -1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • Windows 中的接口函数
#include <winsock2.h>

// 成功返回非负整数,这个整数是客户端的套接字,失败返回 SOCKET_ERROR
SOCKET accept(SOCKET s, struct sockaddr *addr, socklen_t *addrlen);

上述四个步骤是编写 TCP 服务器端的一个基本框架,一步都不能少。如果使用 Windows 编写网络相关的程序,必须使用 winsock2.h 库,并且在编译的时候需要链接 ws2_32。因此 Windows 网络编程代码中需初始化此库,并且在结束的时候还需要注销此库,函数如下

#include <winsock2.h>

// 成功返回 0,失败返回非 0 的错误代码值
int WSAStartup(WORD wVersionRequested, LPWSDATA lpWSAData);

// WORD 表示 winsock 的版本类型,直接传递则需要使用十六进制表示,高 8 位为副版本号,低八位为主版本号,如 0x0102
// 为了方便可以使用 MAKEWORD 函数,只需要两个参数,主版本号和副版本号,如 MAKEWORD(2, 1);
// 第二个参数就是一个 WSADATA 结构体变量的地址,将其传入即可

int WSACleanup(void);

除此之外,Windows 网络编程中关闭套接字也是使用不一样的函数

#include <winsock2.h>

// 成功返回 0,失败返回 SOCKET_ERROR
int closesocket(SOCKET s);

TCP 服务器端代码实现

Linux 中的代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {
  if (2 != argc) {
    fprintf(stderr, "Usage: %s <port>\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  // 1. 创建套接字
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  if (-1 == sockfd) {
    perror("socket() error");
    exit(EXIT_FAILURE);
  }

  // 2. 绑定地址信息
  struct sockaddr_in serv_addr;
  memset(&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  serv_addr.sin_port = htons(atoi(argv[1]));
  if (-1 == bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) {
    perror("bind() error");
    close(sockfd);
    exit(EXIT_FAILURE);
  }

  // 3. 打开可连接状态,进入监听
  if (-1 == listen(sockfd, 5)) {
    perror("listen() error");
    close(sockfd);
    exit(EXIT_FAILURE);
  }

  struct sockaddr_in clnt_addr;
  memset(&clnt_addr, 0, sizeof(clnt_addr));
  socklen_t clnt_len = sizeof(clnt_addr);
  int clntfd = accept(sockfd, (struct sockaddr *)&clnt_addr, &clnt_len);
  if (-1 == clntfd) {
    perror("accept() error");
    close(sockfd);
    exit(EXIT_FAILURE);
  }

  char message[] = "Hello World!";
  write(clntfd, message, sizeof(message));

  close(clntfd);
  close(sockfd);

  return 0;
}

Windows 中的代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>

void error_handling(const char *msg);

int main(int argc, char *argv[]) {
  if (2 != argc) {
    fprintf(stderr, "Usage: %s <port>\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  // 初始化 winsock 库
  WSADATA wsa_data;
  WSAStartup(MAKEWORD(2, 2), &wsa_data);

  // 1. 创建套接字
  SOCKET serv_sock = socket(AF_INET, SOCK_STREAM, 0);
  if (-1 == serv_sock)
    error_handling("socket error");

  // 2. 绑定本地信息
  struct sockaddr_in serv_addr;
  memset(&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  serv_addr.sin_port = htons(atoi(argv[1]));
  if (-1 == bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) {
    closesocket(serv_sock);
    error_handling("bind error");
  }

  // 3. 打开可连接状态
  if (-1 == listen(serv_sock, 5)) {
    closesocket(serv_sock);
    error_handling("listen error");
  }

  // 4. 接收客户端的连接
  struct sockaddr_in clnt_addr;
  int clnt_len = sizeof(clnt_addr);
  SOCKET clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_len );
  if (-1 == clnt_sock) {
    closesocket(serv_sock);
    error_handling("accept error");
  }
  char message[] = "hello world!";
  int size = send(clnt_sock, message, sizeof(message), 0);
  if (-1 == size) {
    closesocket(serv_sock);
    error_handling("send error");
  }

  closesocket(clnt_sock);
  closesocket(serv_sock);
  // 注销 winsock 库
  WSACleanup();
  return 0;
}

void error_handling(const char *msg) {
  fputs(msg, stderr);
  fputc('\n', stderr);
  exit(EXIT_FAILURE);
}

TCP 客户端的基本流程和代码实现

TCP 客户端一般只有两步,如果在 Windows 中编写,还是需要加上库的初始化和最后的注销:

  1. 创建套接字 ——> 购买一个手机
  2. 请求连接 ——> 拨打电话
  • Linux 中的接口实现
#include <sys/types.h>
#include <sys/socket.h>

// 成功返回 0,失败返回 -1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • Windows 中的接口实现
#include <winsock2.h>

// 成功返回 0,失败返回 SOCKET_ERROR
int connect(SOCKET s, const struct sockaddr *addr, socklen_t addrlen);

TCP 客户端代码实现

Linux 中的代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {
  if (3 != argc) {
    fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  // 1. 创建套接字
  int clntfd = socket(AF_INET, SOCK_STREAM, 0);
  if (-1 == clntfd) {
    perror("socket() error");
    exit(EXIT_FAILURE);
  }

  // 2. 发送连接请求
  struct sockaddr_in clnt_addr;
  memset(&clnt_addr, 0, sizeof(clnt_addr));
  clnt_addr.sin_family = AF_INET;
  clnt_addr.sin_addr.s_addr = inet_addr(argv[1]);
  clnt_addr.sin_port = htons(atoi(argv[2]));
  if (-1 == connect(clntfd, (struct sockaddr *)&clnt_addr, sizeof(clnt_addr))) {
    perror("connect() error");
    close(clntfd);
    exit(EXIT_FAILURE);
  } else {
    printf("Connected ......\n");
  }

  char message[1024] = {0};
  int len = read(clntfd, message, 1024);
  if (-1 == len) {
    perror("read() error");
    close(clntfd);
    exit(EXIT_FAILURE);
  }

  printf("read message from server: %s\n", message);
  close(clntfd);

  return 0;
}

Windows 中的代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>

#define BUFFERSIZE 1024

void error_handling(const char *msg);

int main(int argc, char *argv[]) {
  if (3 != argc) {
    fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  // 初始化 winsock 库
  WSADATA wsa_data;
  WSAStartup(MAKEWORD(2, 2), &wsa_data);

  // 1. 创建套接字
  SOCKET clnt_sock = socket(AF_INET, SOCK_STREAM, 0);
  if (-1 == clnt_sock)
    error_handling("socket error");

  // 2. 向服务器发送连接请求
  struct sockaddr_in clnt_addr;
  memset(&clnt_addr, 0, sizeof(clnt_addr));
  clnt_addr.sin_family = AF_INET;
  clnt_addr.sin_addr.s_addr = inet_addr(argv[1]);
  clnt_addr.sin_port = htons(atoi(argv[2]));
  if (-1 == connect(clnt_sock, (struct sockaddr *)&clnt_addr, sizeof(clnt_addr))) {
    closesocket(clnt_sock);
    error_handling("connect error");
  }

  char buffer[BUFFERSIZE] = {0};
  int len = recv(clnt_sock, buffer, BUFFERSIZE, 0);
  if (-1 == len) {
    closesocket(clnt_sock);
    error_handling("recv error");
  }
  
  printf("buffer from server: %s\n", buffer);
  closesocket(clnt_sock);
  // 注销 winsock 库
  WSACleanup();
  return 0;
}

void error_handling(const char *msg) {
  fputs(msg, stderr);
  fputc('\n', stderr);
  exit(EXIT_FAILURE);
}

分别编译运行上述两个程序,先启动服务端的程序,再启动客户端的程序。此时客户端会收到服务端发来的数据,然后两个程序都会立即退出。

上述 Linux 实现的服务器端和客户端都使用 readwrite 函数,是因为在 Linux 中,一切都是文件,socket 也是文件,因此可以使用文件相关的读写操作。而在 Windows 中,网络套接字和文件是有区别的,需要使用网络读写专用的函数 recvsend 来进行操作,这两个函数在 Linux 中也适用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xjjeffery

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值