Socket 编程

目录

理解源 IP 地址和目的 IP 地址

认识端口号 

端口号范围划分 

理解 "端口号" 和 "进程 ID" 

理解源端口号和目的端口号 

理解 socket 

传输层的典型代表

认识 TCP 协议 

认识 UDP 协议 

 网络字节序

socket 编程接口 

socket 常见 API

sockaddr 结构 

 Socket编程UDP

UDP 网络编程

V1 版本 - echo_udp_server

问题 

V2版本-英汉的翻译 dict_server

V3 版本 - 简单聊天室-chat_server

socket编程TCP 

tcp_echo_server 

command_server 


理解源 IP 地址和目的 IP 地址

IP 在网络中,用来标识主机的唯一性

但是这里要思考一个问题:数据传输到主机是目的吗?不是的。因为数据是给人用的。比如:聊天是人在聊天,下载是人在下载,浏览网页是人在浏览?

但是人是怎么看到聊天信息的呢?怎么执行下载任务呢?怎么浏览网页信息呢?通过启动的 qq,迅雷,浏览器。

而启动的 qq,迅雷,浏览器都是进程。换句话说,进程是人在系统中的代表,只要把数据给进程,人就相当于就拿到了数据。

所以:数据传输到主机不是目的,而是手段。到达主机内部,在交给主机内的进程, 才是目的。

但是系统中,同时会存在非常多的进程,当数据到达目标主机之后,怎么转发给目标进程?这就要在网络的背景下,在系统中,标识主机的唯一性。 

认识端口号 

端口号(port)是传输层协议的内容.

(为了能在任意一个主机内部,标识某一个进程的唯一性,在计算机网络里引入了端口号)

1.  端口号是一个 2 字节 16 位的整数;(0到2^16-1)

2.  端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;

3.  IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程;

4.  一个端口号只能被一个进程占用.

端口号范围划分 

1.  0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的. (就像打110就知道报警,打120知道医院救护车)

2.  1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.

理解 "端口号" 和 "进程 ID" 

之前在系统编程的时候, 知道 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?

1.   一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;

2.  进程 ID 属于系统概念,技术上也具有唯一性,确实可以用来标识唯一的一个进程,但是这样做,会让系统进程管理和网络强耦合(例如pid变了那么port也要变,进程每次启动pid都会变),实际设计的时候,并没有选择这样做。主要是让系统与网络解耦;

3.  系统中所有进程都有pid,但并不是所有进程都想网络通信,只有少量的进程需要通信的才需要端口号。所以有端口号的才是想网络通信的。

理解源端口号和目的端口号 

传输层协议(TCP 和 UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁";源端口号和目的端口号在同一个报文中,它们是 TCP 或 UDP 数据报文头部的一部分

理解 socket 

1.  综上,IP 地址用来标识互联网中唯一的一台主机,port 用来标识该主机上唯一的一个网络进程;

 2.  IP+Port 就能表示互联网中唯一的一个进程 ;

3.  所以,通信的时候,本质是两个互联网进程代表人来进行通信,{srcIp, srcPort,dstIp,dstPort}这样的 4 元组就能标识互联网中唯二的两个进程 ;

4.  所以,网络通信的本质,也是进程间通信 ;

5.  我们把 ip+port 叫做套接字 socket;

传输层的典型代表

此处先做了解;

传输层是属于内核的(传输层和网络层),那么我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用,来进行的网络通信。

认识 TCP 协议 

先对 TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;

1.  传输层协议 

2.  有连接 

3.  可靠传输 

4.  面向字节流

认识 UDP 协议 

对 UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;

1.  传输层协议 

2.  无连接 

3.  不可靠传输 

4.  面向数据报

 网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分(大小端是数据的存储方式;), 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出; 

接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存; 

因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.

TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节.

不管这台主机是大端机还是小端机, 都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据; 

如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。 

socket 编程接口 

socket 常见 API

API(Application Programming Interface,应用程序编程接口)是一组定义不同软件组件之间交互规则的接口。简单来说,它是一种使得不同的软件系统、应用程序能够相互通信、共享数据和功能的方式。

C

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)

int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)

int bind(int socket, const struct sockaddr *address, socklen_t address_len);

// 开始监听 socket (TCP, 服务器)

int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)

int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建立连接 (TCP, 客户端)

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockaddr 结构 

socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6,以及后面的 UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.

1.   IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括 16 位地址类型, 16 位端口号和 32 位 IP 地址. 

2.  IPv4、IPv6 地址类型分别定义为常数 AF_INET、AF_INET6. 这样,只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容. 

3.  socket API 可以都用 struct sockaddr *类型表示, 在使用的时候需要强制转化成 sockaddr_in; 这样的好处是程序的通用性, 可以接收 IPv4, IPv6, 以及 UNIX Domain Socket 各种类型的 sockaddr 结构体指针做为参数;

sockaddr 结构 

sockaddr_in 结构 

虽然 socket api 的接口是 sockaddr, 但是我们真正在基于 IPv4 编程时, 使用的数据结 构是 sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP 地址.

in_addr 结构 

in_addr 用来表示一个 IPv4 的 IP 地址. 其实就是一个 32 位的整数;

 Socket编程UDP

UDP比较简单,建立好后就可以连接了。 

UDP 网络编程

V1 版本 - echo_udp_server

简单的回显服务器和客户端代码 备注: 代码中会用到地址转换函数

socket函数:

#include <sys/types.h>
 #include <sys/socket.h>

 int socket(int domain, int type, int protocol);

创建套接字(socket)的作用是为网络通信提供一个通信端点。套接字是应用程序与网络协议栈(如 TCP/IP 协议栈)之间的接口,它允许程序通过网络发送和接收数据。通过套接字,应用程序可以与其他机器或同一机器上的不同进程进行数据交换。 

验证文件描述符_socket是3;

bind 绑定端口号:一个用于将一个套接字(socket)与一个特定的地址绑定的系统调用。在网络编程中,通常用于在服务端应用中指定服务器的地址和端口。

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


bind() 函数通常用于服务端在使用 UDP 或 TCP 协议时,绑定到一个特定的端口和地址。客户端则不需要调用 bind(),因为它通常会让操作系统自动为它选择一个可用端口。

 ip字符串转化网络字节序列

验证绑定成功: 

显示当前系统的网络连接、套接字以及相关信息,查看udp信息:

如果信息不全可以试试sudo,用管理员权限

recvfron函数:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

recvfrom 是一个系统调用,通常用于在网络编程中接收数据。它属于 套接字(socket)编程 的一部分,特别是用于 UDP 套接字(虽然它也可以用于其他协议)。recvfrom 函数能够接收来自远程主机的消息,并且除了接收到的数据,还可以获取消息的源地址和端口。 

 sendto 是一个在 C 语言中用于向指定的目标地址发送数据的函数。它通常用于无连接的通信方式,例如 UDP 套接字,在这种情况下,每次发送数据时都需要指定目标地址。

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

127.0.0.1 是回环地址(Loopback address),通常用于在本地计算机内部进行通信,指代本机网络接口。它不会通过物理网络接口(如Wi-Fi或有线网络)传输数据,而是仅在本机的网络栈内进行处理。

"127.0.0.1 不走网络" 是指它不会通过外部网络通信或外部路由,这实际上是正常现象。具体来说,127.0.0.1 的数据流只会在本地机器上进行,不会通过任何外部网络硬件(如路由器或交换机)。

问题 

client发送消息,但是server收到不打印
原因:client的port没有序列化

 服务端绑定自己的云服务器ip地址失败
因为:云服务器上,服务端不能直接(也强烈不建议)bind自己的公网ip!因为这是虚拟出来的。可以绑定自己的服务器内网ip,但能不能通信就不一定了;

绑定内网了那么代表你的服务器不能从外网上收消息了,因为不会在公网上暴漏;
我们建议在服务器端绑定ip地址时为0


绑定固定IP地址则只能收到固定IP地址的报文

inet_ntoa 是一个标准的 C 库函数,定义在 <arpa/inet.h> 头文件中,主要用于将一个 IPv4 地址(以网络字节顺序表示的二进制格式)转换为点分十进制的字符串格式。 该函数仅支持 IPv4 地址,不适用于 IPv6 地址。

client的端口号,一般不让用户自己设定,而是让client os随机选择

client需要bind他自己的IP和端口,但是client不需要”显示“bind他自己的IP和端口

client在首次向服务器发送数据的时候(例如sendto时),os会自动给client bind他的IP和端口

V2版本-英汉的翻译 dict_server

如果只是简单的你发消息我回显给你,那么就单调了。

V3 版本 - 简单聊天室-chat_server

目前用的是单进程的,没有做其他的设计。那么我们可以用多进程-线程池引入,变为网络消息转发的模块;

原理:

socket编程TCP 

tcp_echo_server 

在网络编程中,listen() 是一个用于服务器端套接字的函数,它用于将一个已经绑定到特定 IP 地址和端口的套接字转换为一个 监听套接字,以便接收客户端的连接请求。listen() 函数告诉操作系统,这个套接字将用于监听连接请求,并准备接收传入的连接。调用 listen() 之后,套接字会进入 监听状态,并等待客户端的连接请求。

accept() 函数是服务器端在处理客户端连接时使用的一个重要函数。它用于从一个已监听的套接字(通过 listen() 函数设置)接受一个传入的连接请求,并返回一个新的套接字描述符用于与该客户端进行通信。

  • accept() 是一个关键函数,用于服务器从客户端接收连接。它会阻塞,直到客户端请求连接并且可以接受为止。
  • 成功调用 accept() 会返回一个新的套接字,这个套接字是与客户端之间的通信通道。
  • addr 和 addrlen 允许服务器端获取客户端的网络地址信息,通常是客户端的 IP 和端口。

tcp和udp的sockfd不同:

在udp的时候从头到位一直使用一个文件描述符sockfd来进行数据的接收和发送,而tcp不同,有多个,如果多个连接则有多个new_sockfd;UDP 使用单一的套接字描述符来处理所有数据的收发,而 TCP 需要为每个连接创建独立的套接字描述符(new_sockfd)。

UDP 是面向数据报的协议(你收的一定是别人发的),它通常使用与之相关的 sendtorecvfrom 函数来进行数据传输。这些函数是 UDP 进行通信时常用的接口,区别于 TCP 的 sendrecv。这主要是由于 UDP 是一个 无连接面向数据报 的协议,他无法直接做文件读取;而tcp是面向字节流的可以使用read和write进行文件读取和发送;

connect 函数用于在客户端程序中建立一个到指定远程地址的网络连接。它是一个系统调用,通常用于建立到服务器的 TCP 连接。connect 函数在成功时返回 0,失败时返回 -1,并设置 errno 以指示错误原因。

version 1 --- 多进程版本: 

command_server 

popen 是 C 标准库中的一个函数,允许你通过创建一个子进程来执行命令并与该子进程进行输入输出(I/O)通信。它的功能是打开一个管道,从而使程序能够读取子进程的输出或向子进程写入数据。popen 函数返回一个文件指针,你可以使用 fread, fwrite, fprintf, fscanf 等 I/O 函数来进行数据操作。

使用 popen 的步骤:

  1. 调用 popen 打开一个管道,指定要执行的命令和模式。
  2. 使用文件指针与子进程进行通信,例如通过 fread 从子进程读取输出,或者通过 fprintf 向子进程发送输入。
  3. 完成操作后,调用 pclose 关闭管道并等待子进程的终止。

fgets 是 C 标准库中的一个函数,用于从文件流中读取一行数据。它读取字符直到遇到换行符(\n)或者达到指定的字符数限制为止。fgets 函数是处理文本输入时非常常用的一个函数,因为它能够有效防止缓冲区溢出。

fgets 会包括换行符 '\n',如果读取时遇到换行符,它会被包含在返回的字符串中。

fgets 会自动在字符串末尾加上 '\0',使其成为合法的 C 字符串。 

效果图: 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

wangsir.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值