Linux网络编程(一):从一个简单的程序开始

看了很久Linux网络编程,代码都基本能看懂,但是自己一写就废,菜是原罪/(ㄒoㄒ)/~~,所以准备写一个系列关于自己学习Linux网络编程的过程。

说明:之后的开发环境如下:

  • 系统: Ubuntu 20.04.1 LTS
  • 语言:C/C++
  • 编译器:g++
  • IED:CLion 2020.2.1

0. 从一个简单的程序开始

0.1 服务器接收

需求:实现服务器接收第一个客户端发送过来的第一个数据,将其打印后关闭连接,并退出程序。

  • 服务器端代码实现如下:
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cassert>
#include <cerrno>
#include <unistd.h>

int main(int argc, char** argv) {
  if(argc <= 2) {
    printf("usage: %s ip port\n", basename(argv[0]));
    return 1;
  }

  const char* ip = argv[1];
  int port = atoi(argv[2]);

  int ret = 0;
  struct sockaddr_in addr{};
  bzero(&addr, sizeof(addr));
  addr.sin_family = AF_INET;
  inet_pton(AF_INET, ip, &addr.sin_addr);
  addr.sin_port = htons(port);

  // 创建socket
  int listenfd = socket(PF_INET, SOCK_STREAM, 0);
  assert(listenfd != -1);

  // 绑定端口
  ret = bind(listenfd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
  assert(ret != -1);

  // 监听
  ret = listen(listenfd, 5);
  assert(ret != -1);

  struct sockaddr_in client{};
  socklen_t client_addr_len = sizeof(client);
  // 等待连接, 此处将会阻塞,直到有一个客户端连接到来
  int connfd = accept(listenfd, reinterpret_cast<struct sockaddr*>(&client), &client_addr_len);
  if(connfd <= 0) {
    printf("connection error, errno is:%d\n", errno);
    exit(1);
  }

  printf("连接成功, 客户端地址:[%s:%d]\n", inet_ntoa(client.sin_addr), client.sin_port);

  char buffer[1024] = {0};
  // 接收数据,此处将会阻塞,直到有数据到来
  ret = recv(connfd, buffer, sizeof(buffer) - 1, 0);
  printf("接受到%d字节数据, 内容为:%s\n", ret, buffer);

  close(connfd);
  close(listenfd);

  return 0;
}

上面代码的运行需要输入地址和端口号两个参数,在CLion中指定该两个参数步骤如下:
在这里插入图片描述
运行上面的代码,它会阻塞在accept()函数,直到有连接到来后,接着运行到recv()函数,在此处阻塞直到有数据到来。
客户端给服务的发送数据我们用python来做,步骤如下:

>>> import socket
>>> sock = socket.socket()
>>> sock.connect(("192.168.148.20", 12345)) # 和服务端建立了连接
>>> sock.send(bytes("Hello", encoding="utf-8")) # 发送数据

服务端运行结果如下:

连接成功, 客户端地址:[192.168.148.20:51841]
接受到5字节数据, 内容为:Hello

0.2 服务器接收与发送

现在,我们增加一个需求:在收到客户端数据后,服务器回应一条数据再关闭。大致的步骤与之前的没有区别,所以我们只增加服务器回应的部分,在recv()之后添加:

  // 服务器做出回应
  bzero(&buffer, sizeof(buffer));
  snprintf(buffer, sizeof(buffer), "This is from server.");
  ret = send(connfd, buffer, strlen(buffer), 0);
  printf("发送%d字节数据, 内容为:%s\n", ret, buffer);

再用python来充当客户端测试:

>>> import socket
>>> sock = socket.socket()
>>> sock.connect(("192.168.148.20", 12345))
>>> sock.send(bytes("Hello", encoding="utf-8"))
5
>>> data = sock.recv(1024)
>>> print(data.decode())
This is from server.
>>> 

到此,服务器发送和接收数据都大概了解了。但是如何让服务器一直接收和发送,而不是收到/发送一条数据后就关闭呢?

0.3 服务器不断发送和接收

现在,我们再加一个需求:要求服务器不断接收/发送数据。其实改动不大,只需要将接收数据的代码块放到一个while循环中,循环终止的条件是客户端断开(recv()的返回值<= 0),代码如下:

......
  char buffer[1024] = {0};
  while (1) {
    // 接收数据,此处将会阻塞,直到有数据到来
    bzero(&buffer, sizeof(buffer));
    ret = recv(connfd, buffer, sizeof(buffer) - 1, 0);
    if(ret <= 0) {
      break;
    }
    printf("接受到%d字节数据, 内容为:%s\n", ret, buffer);

    // 服务器做出回应
    bzero(&buffer, sizeof(buffer));
    snprintf(buffer, sizeof(buffer), "This is from server.");
    ret = send(connfd, buffer, strlen(buffer), 0);
    printf("发送%d字节数据, 内容为:%s\n", ret, buffer);
  }
......

现在我们再用python来测试一下:

>>> import socket
>>> sock = socket.socket()
>>> sock.connect(("192.168.148.20", 12345))
>>> sock.send(bytes("Hello", encoding="utf-8"))
5
>>> data = sock.recv(1024)
>>> data
b'This is from server.'
>>> sock.send(bytes("I am Damon", encoding="utf-8"))
10
>>> data = sock.recv(1024)
>>> data
b'This is from server.'
>>> sock.close()
>>> 

服务器端的输出如下:

连接成功, 客户端地址:[192.168.148.20:56449]
接受到5字节数据, 内容为:Hello
发送20字节数据, 内容为:This is from server.
接受到10字节数据, 内容为:I am Damon
发送20字节数据, 内容为:This is from server.

那么,如果有多个客户端同时连接服务器怎么办呢?那就是IO复用的部分了,这部分我们下个章节再讲。

0.4 本章用到的API

在此之前先补充一个知识:字节序的大端小端。所谓大端小端指的是数据在内存中的排列顺序,大端:数据的低位放到内存高地址;小端:数据的地位放到内存低地址。还有个概念:网络字节序指的是大端(操作系统一般用小端)。一个测试代码如下:

void TestByteOrder() {
  union {
    short value;
    char union_bytes[sizeof(short)];
  } test;

  test.value = 0x0102;
  // 大端: 低地址存放高位(看尾部是否在高地址)
  // 小端: 低地址存放低位(看尾部是否在低地址)
  if(test.union_bytes[0]/*低地址*/ == 1/*高位*/ && test.union_bytes[1]/*高地址*/ == 2) {
    printf("大端\n");
  } else if(test.union_bytes[0]/*低地址*/ == 2/*低位*/ && test.union_bytes[1]/*高地址*/ == 1) {
    printf("小端\n");
  } else {
    printf("unknow\n");
  }
}
0.4.1 socket地址相关

表示socket地址使用的是结构体,分为“通用socket地址”和“专用socket地址”。

0.4.1.1 通用socket地址
#include <bits/socket.h>
struct sockaddr {
	sa_family_t sa_family;	// PF_* AF_*(如PF_INET, AF_INET)
	char sa_data[14];		// 存放socket地址
};
0.4.1.2 专用socket地址
struct sockaddr_in {
	sa_family_t sa_family;	// 地址簇: AF_INET
	u_int16_t sin_port;		// 端口号,要用网络字节序表示
	struct in_addr sin_addr;// IPv4地址结构体
};
struct in_addr{
	u_int32_t s_addr;		// IPv4地址,要用网络字节序表示
};

注意:所有专用socket地址类型的变量在使用时都要转成通用socket地址类型sockaddr(比如前面bind()函数的第二个参数)。

实例如下:

  struct sockaddr_in addr{};
  bzero(&addr, sizeof(addr));
  addr.sin_family = AF_INET;
  inet_pton(AF_INET, ip, &addr.sin_addr);
  // 或者用下面的
  // addr.sin_addr.s_addr = htonl(INADDR_ANY);
  addr.sin_port = htons(port);
0.4.2 IP地址转换函数

所谓的地址转换是“十进制IPv4地址”和“网络字节序整数”之间的转换(因为使用api时用的时网络字节序的整数)。

0.4.2.1 IP->整数

将用点分十进制字符串表示的IPv4地址转换成网络字节序整数,需要用到inet_pton函数,API描述如下:

extern int inet_pton (int __af, const char *src, void *dst);
  • af: 指定地址簇(AF_INET);
  • src: 用点分十进制字符串表示的IPv4地址;
  • dst: 保存转换成网络字节序整数的结果;
  • 返回值:成功返回1,失败返回0并设置errno

其使用如下:

#include <arpa/inet.h>
  const char* ip = "127.0.0.1";
  uint32_t adr;
  int res = inet_pton(AF_INET, ip, &adr);
  if(res == 1) {
    printf("转换成功:点分十进制地址[%s]->网络字节序整数[%d], 返回值:%d\n", ip, adr, res);
  } else if (res == 0){
    printf("转换失败!, errno:%d", errno);
  }
0.4.2.1 整数->IP

将用网络字节序整数地址转换成点分十进制字符串表示的IPv4地址,需要用到inet_ntop函数,API描述如下:

 const char *inet_ntop (int af, const void *src,char *dst, socklen_t len);
  • af: 指定地址簇(AF_INET);
  • src: 网络字节序整数表示的地址;
  • dst: 点分十进制字符串表示的IPv4地址;

用法:

 struct sockaddr_in addr{};
 bzero(&addr, sizeof(addr));
 addr.sin_family = AF_INET;
 inet_pton(AF_INET, ip, &addr.sin_addr);
 addr.sin_port = htons(port);

 char bf[INET_ADDRSTRLEN]; // 该值为14
 const char* result = inet_ntop(AF_INET, &addr.sin_addr, bf, INET_ADDRSTRLEN);
 printf("网络字节序整数[%d]->点分十进制地址[%s]", addr.sin_addr.s_addr, result);
0.4.3 创建socket
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • domain: 底层协议簇, 一般都填PF_INET(IPv4)的;
  • type: TCP填SOCK_STREAM,UDP填SOCK_UGRAM;
  • protocol: 一般填0;
  • 返回值:成功返回socket fd,失败返回-1并设置errno
    用法:
int sock_fd = socket(PF_INET, SOCK_STREAM, 0);
// 在Linux 2.6.17之后可用下面将新建的socket设置为非阻塞的
int sock_fd = socket(PF_INET, SOCK_STREAM & SOCK_NONBLOCK, 0);
0.4.4 绑定socket
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
  • sockfd: 未命名的文件描述符;
  • my_adr: 地址(注意要将其转换成struct sockaddr *)
  • addrlen: sizeof(my_addr);
  • 返回值: 成功返回0,失败返回-1并设置errno

用法:

  // 绑定端口
int ret = bind(listenfd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
assert(ret != -1);
0.4.5 监听socket
#include <sys/socket.h>
int listen(int sockfd, int backlog/*典型值为5*/);
  • sockfd: 指定被监听的socket;
  • backlog: 提示内核监听队列的最大长度,监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息;
  • 返回值:成功返回0,失败返回-1并设置errno
0.4.6 接受连接
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
  • sockfd: 执行过listen系统调用的监听socket;
  • addr: 用来获取被接受连接的远端socket地址;
  • addrlen: addr的长度(注意此处为socklen_t *,指针类型;
  • 返回值:成功返回连接fd,失败返回-1并设置errno

用法:

 struct sockaddr_in client{};
 socklen_t client_addr_len = sizeof(client);
 // 等待连接, 此处将会阻塞,直到有一个客户端连接到来
 int connfd = accept(listenfd, reinterpret_cast<struct sockaddr*>(&client), &client_addr_len);
0.4.7 发起连接

之前部分为服务器上的操作,现在的是客户端要做的:向服务器发起连接。

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
  • sockfd: 又socket系统调用返回的一个socket;
  • serv_addr: 服务器监听的socket地址;
  • addrlen: serv_addr的长度;
  • 返回值:成功返回0,失败返回-1并设置errno

一旦成功建立连接,sockfd就唯一标识了这个连接,客户端可通过读写sockfd来与服务器通信。

0.4.8 关闭连接
#include <unistd.h>
int close(int fd);
  • fd: 待关闭的socket。

注意:close系统调用并非总是立即关闭一个连接的,而是将fd的引用计数减1.只有当fd的引用计数为0时,才真正关闭连接。(比如多进程中,一次fork系统调用默认将父进程中打开的socket的引用计数加1,所以必须在父进程和子进程中都对该socket执行close调用才能将连接关闭)。

如果无论如何都要立即终止连接,而不是将socket的引用计数减1,使用shutdown系统调用:

#include <sys/socket.h>
int shutdown(int sockfd, int howto);
  • sockfd: 待关闭的socket;
  • howto: SHUT_RD(关闭sockfd上读的一半,应用程序不能再读,且接收缓冲区中数据全部丢弃);SHUT_WR(关闭sockfd上写的一半); SHUT_RDWR(同时关闭读写)。
  • 返回值:成功返回0,失败返回-1并设置errno
0.4.9 TCP数据读写

对文件的读写操作readwrite虽然也可以用于socket,但是socket编程通常还是使用一些专门用于网络编程的系统调用,下面两个是TCP流数据的读写系统调用:

#include <sys/socket.h>
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
  • sockfd: 通常是服务器/客户端使用accept/connect后得到的;
  • buf: 读缓冲区位置;
  • len: 读缓冲区大小;
  • flags: 额外的控制,通常填0即可(只对当前调用有效,永久修改使用setsockopt)。
  • 返回值:成功时返回实际读取到的数据长度(可能小于我们期望的长度len,因此需要多次调用recv)。为0时对方已关闭连接(收到FIN),出错时返回-1并设置errno

用法:

char buffer[1024]={0};
int ret = recv(connfd, buffer, sizeof(buffer)-1, 0);	// 注意,需要留1B存`\0`

发送数据:

ssize_t send(int sockfd, const void* buf, size_t len, int flags);
  • sockfd: 通常是服务器/客户端使用accept/connect后得到的;
  • buf: 缓冲区位置;
  • len: 写缓冲区大小;
  • flags: 额外的控制,通常填0即可(只对当前调用有效,永久修改使用setsockopt)。
  • 返回值:成功时返回实际写入的数据长度,失败返回-1并设置errno

用法:

const char* data = "Hello";
int ret = send(connfd, data, strlen(data), 0);	// 注意,需要留1B存`\0`

注意:recv中一般用sizeof(),而send中一般用strlen()

0.4.10 UDP数据读写

用于UDP数据报读写的系统调用如下:

#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);

参数和TCP的类似,不过因为UDP没有连接的概念,所以每次发送/接受都需要对方的socket地址。如果最后两个参数设置为NULL,那也可以当作recv()send()来用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux服务器端网络收发程序可以使用 C 或 Python 编写。以下是一个使用 C 语言编写的简单示例: ``` #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> int main(int argc , char *argv[]) { int socket_desc , client_sock , c , read_size; struct sockaddr_in server , client; char client_message[2000]; // 创建套接字 socket_desc = socket(AF_INET , SOCK_STREAM , 0); if (socket_desc == -1) { printf("Could not create socket"); } puts("Socket created"); // 准备地址 server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = htons( 8888 ); // 绑定 if( bind(socket_desc,(struct sockaddr *)&server , sizeof(server)) < 0) { //print the error message perror("bind failed. Error"); return 1; } puts("bind done"); // 监听 listen(socket_desc , 3); // 接受客户端连接 puts("Waiting for incoming connections..."); c = sizeof(struct sockaddr_in); // 接受客户端套接字 client_sock = accept(socket_desc, (struct sockaddr *)&client, (socklen_t*)&c); if (client_sock < 0) { perror("accept failed"); return 1; } puts("Connection accepted"); // 接收客户端消息 while( (read_size = recv(client_sock , client_message , 2000 , 0)) > 0 ) { // 发送消息回客户端 write(client_sock , client_message , strlen(client_message)); } if(read_size == 0) { puts("Client disconnected"); fflush(stdout); } else if(read_size == -1) { perror("recv failed"); } return 0; } ``` 这个程序使用了 Linux 的套接字编程来创建一个网络服务器。它监听来自客户端的请

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值