socket 编程

采用 socket 实现服务端和客户端通信

server.c

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

#define BUF_SIZE 1024
#define MAX_PENDING 20

int main() {
  int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (serv_sock == -1) {
    perror("socket() error");
    exit(EXIT_FAILURE);
  }

  struct sockaddr_in serv_addr;
  // 也可以使用 bzero(&serv_addr, sizeof(serv_addr));
  memset(&serv_addr, 0, 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(1234);

  if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
    perror("bind() error");
    close(serv_sock);
    exit(EXIT_FAILURE);
  }

  if (listen(serv_sock, MAX_PENDING) == -1) {
    perror("listen() error");
    close(serv_sock);
    exit(EXIT_FAILURE);
  }

  char buffer1[BUF_SIZE];
  char buffer2[BUF_SIZE];

  while (1) {
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    int clnt_sock =
        accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1) {
      perror("accept() error");
      continue;
    }

    memset(buffer2, 0, BUF_SIZE);
    printf("Enter a message: ");
    fgets(buffer2, BUF_SIZE, stdin);
    write(clnt_sock, buffer2, strlen(buffer2));

    memset(buffer1, 0, BUF_SIZE);
    read(clnt_sock, buffer1, BUF_SIZE);
    printf("Client: %s\n", buffer1);

    close(clnt_sock);
  }

  close(serv_sock);
  return 0;
}

client.c

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

#define BUF_SIZE 1024

int main() {
  struct sockaddr_in serv_addr;
  memset(&serv_addr, 0, 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(1234);

  char buffer1[BUF_SIZE];
  char buffer2[BUF_SIZE];

  while (1) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == -1) {
      perror("socket() error");
      continue;
    }

    if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
      perror("connect() error");
      close(sock);
      continue;
    }

    memset(buffer1, 0, BUF_SIZE);
    read(sock, buffer1, BUF_SIZE);
    printf("Server: %s\n", buffer1);

    printf("Enter a message: ");
    memset(buffer2, 0, BUF_SIZE);
    fgets(buffer2, BUF_SIZE, stdin);
    write(sock, buffer2, strlen(buffer2));

    close(sock);
  }

  return 0;
}

这两个程序中的知识点:

socket 函数

函数原型:int socket(int af, int type, int protocol);

(1) af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET (IPv4)和 AF_INET6(IPv6)

也可以使用 PF 前缀,PF 是“Protocol Family”的简写,它和 AF 是一样的。例如,PF_INET 等价于 AF_INET,PF_INET6 等价于 AF_INET6

(2) type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)

(3) protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议

一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。如果只有一种情况,protocol 可以用 0 来代替,例如:

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字

perror函数

perror讲解

EXIT_FAILURE

定义EXIT_FAILURE 是一个宏,定义在头文件 stdlib.h 中。

用途:它通常用于 exit() 函数的参数,表示程序因某种失败条件而终止。

:虽然其具体的值可能因实现而异,但通常它的值为 1。这与成功退出的常量 EXIT_SUCCESS(通常值为 0)形成对比。

sockaddr_in

sockaddr_in 定义:

// 在 <netinet/in.h> 定义
struct sockaddr_in {
    short            sin_family;   // 地址族(Address family),对于IPv4,其值为AF_INET
    unsigned short   sin_port;     // 端口号,使用网络字节序,一般在 1024 - 65536 中选择
    struct in_addr   sin_addr;     // IPv4地址结构体
    char             sin_zero[8];  // 用于将此结构体的大小与`sockaddr`对齐,通常设置为0
};

in_addr

// 在 <netinet/in.h> 定义
struct in_addr {
    uint32_t s_addr;  // IPv4地址,使用网络字节序
};

inet_ntoa函数:将一个网络字节序的 IPv4 地址转换为点分十进制格式的字符串

#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);

返回一个指向点分十进制格式的 IPv4 地址字符串的指针。这个字符串是静态的,所以每次调用 inet_ntoa 时都会被覆盖。因此,如果你需要保留这个地址,你应该复制它到另一个字符串中。

sockaddr

struct sockaddr{
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址类型
    char         sa_data[14];  //IP地址和端口号
};

地址转化

使用 inet_addr()inet_pton() 函数将点分十进制的IPv4地址字符串转换为网络字节序的整数形式。

使用 inet_ntoa()inet_ntop() 函数将网络字节序的整数转换为点分十进制的IPv4地址字符串。

struct in_addr ip_addr;
ip_addr.s_addr = inet_addr("192.168.1.1");  // 将点分十进制字符串转换为网络字节序的整数

bind 函数

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);

socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。

为什么使用 sockaddr_in 而不使用 sockaddr:

sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。

网络字节序转换

网络字节序统一为大端序

htons() 用来将当前主机字节序转换为网络字节序,其中h代表主机(host)字节序,n代表网络(network)字节序,s代表short,htons 是 h、to、n、s 的组合,可以理解为”将 short 型数据从当前主机字节序转换为网络字节序“。

常见的网络字节转换函数有:

  • htons():host to network short,将 short 类型数据从主机字节序转换为网络字节序。
  • ntohs():network to host short,将 short 类型数据从网络字节序转换为主机字节序。
  • htonl():host to network long,将 long 类型数据从主机字节序转换为网络字节序。
  • ntohl():network to host long,将 long 类型数据从网络字节序转换为主机字节序。

通常,以s为后缀的函数中,s代表 2 个字节 short,因此用于端口号转换;以l为后缀的函数中,l代表 4 个字节的 long,因此用于 IP 地址转换。

connect 函数

connect 函数用来将 client 端和 server 端建立连接:

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);

listen 函数

int listen(int sock, int backlog);

通过 listen() 函数可以让套接字进入被动监听状态,所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。

当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。

如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。

accept 函数

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

accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

fgets 函数

fgets讲解

read/write 函数

ssize_t read(int fd, void *buf, size_t nbytes);

ssize_t write(int fd, const void *buf, size_t nbytes);

fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。

read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。

客户端将服务端的一个文件克隆下来

server.c

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

#define BUF_SIZE 1024
#define MAX_PENDING 20

int main() {
  const char* fileName = "hh";
  FILE* fp = fopen(fileName, "r");
  if (fp == NULL) {
    perror("open file failed!");
    exit(EXIT_FAILURE);
  }

  int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (serv_sock == -1) {
    perror("socket() error");
    exit(EXIT_FAILURE);
  }

  struct sockaddr_in serv_addr;
  memset(&serv_addr, 0, 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(1234);

  if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
    perror("bind() error");
    close(serv_sock);
    exit(EXIT_FAILURE);
  }

  if (listen(serv_sock, MAX_PENDING) == -1) {
    perror("listen() error");
    close(serv_sock);
    exit(EXIT_FAILURE);
  }

  struct sockaddr_in clnt_addr;
  socklen_t clnt_addr_size = sizeof(clnt_addr);
  int clnt_sock =
      accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
  if (clnt_sock == -1) {
    perror("accept() error");
    exit(EXIT_FAILURE);
  }

  char buffer[BUF_SIZE] = {0};
  int cnt;
  while ((cnt = fread(buffer, 1, BUF_SIZE, fp)) > 0) {
    write(clnt_sock, buffer, cnt);
  }

  shutdown(clnt_sock, SHUT_WR);
  read(clnt_sock, buffer, BUF_SIZE);

  fclose(fp);
  close(clnt_sock);
  close(serv_sock);

  return 0;
}

client.c

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

#define BUF_SIZE 1024

int main() {
  const char* fileName = "save";
  FILE* fp = fopen(fileName, "w");
  if (fp == NULL) {
    perror("open file failed!");
    exit(EXIT_FAILURE);
  }

  int sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock == -1) {
    perror("socket() error");
    exit(EXIT_FAILURE);
  }

  struct sockaddr_in serv_addr;
  memset(&serv_addr, 0, 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(1234);

  if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
    close(sock);
    perror("connect() error");
    exit(EXIT_FAILURE);
  }

  char buffer[BUF_SIZE] = {0};
  int cnt;
  while ((cnt = read(sock, buffer, BUF_SIZE)) > 0) {
    fwrite(buffer, 1, cnt, fp);
  }

  fclose(fp);
  close(sock);

  return 0;
}
posted @   hacker_dvd  阅读(13)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示