目录
一、socket 套接字概述
二、socket 函数接口
三、IP地址与端口号的网络格式
四、TCP协议的本地通信C语言示例
一、socket 套接字概述
socket 是什么?
socket 本质上是一个抽象的概念,它是一组用于网络通信的 API,提供了一种统一的接口,使得应用程序可以通过网络进行通信。在不同的操作系统中,socket 的实现方式可能不同,但它们都遵循相同的规范和协议,可以实现跨平台的网络通信。
socket 实现通信的原理是基于网络协议栈。
当应用程序创建一个 socket 并指定协议族、类型和使用的协议后,操作系统会创建一个对应的套接字,并把它加入到协议栈中。
协议栈是一个由多个层次协议组成的网络协议体系结构,它负责对数据进行封装和解封装,并确保数据能够在网络上正确传输。
当应用程序通过 socket 发送数据时,操作系统会将数据传递给协议栈的上层协议,该协议会对数据进行封装并添加一些必要的信息,例如目标 IP地址和端口号等。然后将封装后的数据传递给下一层协议,直到数据最终被封装成一个网络包并通过网络发送到目标主机。
当目标主机收到网络包后,协议栈会对数据进行解封装,并将数据传递给操作系统中的套接字。如果该套接字是一个监听套接字,操作系统会创建一个新的套接字来处理连接请求,并将新的套接字加入到协议栈中。如果该套接字是一个已连接套接字,操作系统会将数据传递给应用程序处理。
总之,socket 实现通信的原理是基于网络协议栈,通过将数据封装成网络包并通过网络传输,实现了应用程序之间的通信。操作系统负责管理套接字和协议栈,确保数据能够正确传输。
在Linux中,socket是一种文件类型,伪文件,不占用存储空间,可进行IO操作,可间接看做文件描述符使用。
socket 通信流程图:
客户端与服务器工作的核心逻辑:
- 在客户端向服务器发送请求之前,服务器必须已经初始化完成。
- 客户端和服务器的初始化,都需要创建套接字socket,设置IP地址结构体信息。
- 服务端在设置完IP地址结构体信息之后,需要bind绑定套接字,通过listen将socket设置为监听状态。
- 客户端通过connect向服务端发送连接请求,服务端通过accept接收客户端的连接请求,接收成功后获取新的套接字文件描述符。(TCP三次握手)
- 客户端发送数据——向文件描述符写入数据write,服务端接收数据——从文件描述符读出数据read,服务端回射数据write,客户端获取回射数据read。
- 客户端或服务端发送通信结束信号,close文件描述符,结束通信。(TCP四次挥手)
二、socket 函数接口
socket():创建套接字,返回一个可操作性的文件描述符
int socket(int domain,int type,int protocol);
参数一:表示ip地址类型,常用的有两种
- 其中AF_INET表示IPv4地址,比如127.0.0.1,这是一个本地 ip
- 其中AF_INET6表示IPv6地址,比如2001:3CA1:10F:1A:121B:0:0:10
参数二:表示数据传输方式/套接字类型,常见两种
- SOCK_DGRAM (数据报套接字/无连接的套接字,UDP)
- SOCK_STREAM(流格式套接字/面向连接的套接字,TCP)
参数三:表示传输协议
- 理论上前两个参数已经可以推演出采用哪种协议,可以将protocol 的值设为 0,系统自动推演出采用哪种协议
返回值:返回一个套接字(文件描述符fd)
bind():用于服务器,给sockfd套接字绑上本机地址和使用端口,确定了服务器的身份
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数一:套接字的fd(文件描述符),socket()函数的返回值
参数二:结构体 ip+port(端口)
参数三:结构体的字节长度
返回值:判断绑定成功失败
listen():用于服务器,使socket处于监听模式,监听时候有客户端连接,并放入队列(同时设置与服务器建立连接的上限)
int listen(int sockfd, int backlog);
参数一:bind绑定ip和端口的套接字
参数二:请求链接客户端队列的最大存放数目
返回值:判断监听成功失败
accept():用于服务器,接收一个客户端的连接请求,并返回连接客户端的套接字便于IO操作,如果没有客户连接会阻塞等待
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数一:服务器的套接字(也叫监听套接字),表明了自己的身份
参数二:传出参数,跟我建立连接的客户端的结构体(内含客户端ip+端口)
参数三:结构体长度的指针 &sizeof()
返回值:连接客户端的套接字
connect():用于客户端,向远端服务器发送连接请求
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数一:传入参数,客户端对服务器进行IO操作的文件描述符
参数二:绑定需要连接的服务器的结构体(需要初始化绑上ip和端口),表明目的
参数三:结构体的长度
三、IP地址与端口号的网络格式
IP地址:
- IP一般由32位整数组成,按每8位划分为4部分:255.255.255.255 该显示方式为字符串形式,而IP一般是以整数形式显示。
- 整数IP地址 unsigned int IP_Addr = 1713350848 转化为二进制为:01100110-00011111-10101000-11000000 根据8位划分得到结果为102-31-168-192,由于网络字节倒序的问题,实际IP为192.168.31.102
- 在网络通信中,我们输入的是字符串风格的IP地址字符串,这时需要我们将IP地址字符串转换成 uint32_t 的类型进行网络通信,建议直接用库函数 inet_addr(const char* ip) 进行转换。
- 对于服务器而言,bind绑定套接字的时候,IP地址可用 htonl(INADDR_ANY) 进行任意地址绑定。
// 整数风格 uint32_t 转 字符串风格 ip:
// uint32_t ip;
// struct _ip {
// unsigned char p1;
// unsigned char p2;
// unsigned char p3;
// unsigned char p4;
// };
// std::string strip = to_string(((struct _ip*)&ip)->p1) + to_string(((struct _ip*)&ip)->p2) +
// to_string(((struct _ip*)&ip)->p3) + to_string(((struct _ip*)&ip)->p4);
//
// 字符串风格 转 整数风格:
// 整数风格的ip地址存储方式,占用空间更小,网络通信都使用 uint32_t 的ip格式
// 系统提供的转换方式:inet_addr(const char* ip)
// 任意地址绑定:htonl(INADDR_ANY)
端口号port:
- 端口号port的数据类型是uint16_t,但是在网络通信中的类型是in_port_t,这其实是uint16_t的重命名,但我们还是需要将port端口号从主机格式转换为网络格式。
- 通过 htons(port) 将端口号信息绑定到 struct sockaddr_in 结构体中。
四、TCP协议的本地通信C语言示例
server.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int args, char* argv[])
{
if (args != 2)
{
printf("Usage:server port\n");
exit(1);
}
uint16_t port = atoi(argv[1]); // 启动server的时候指定端口号
// 1. 创建套接字
int s_socket = socket(AF_INET, SOCK_STREAM, 0);
if (s_socket < 0)
exit(1);
// 2. 绑定套接字
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY); // Address to accept any incomint message ---> 任意地址绑定
local.sin_port = htons(port); // 主机转网络
if (bind(s_socket, (struct sockaddr*)&local, sizeof(local)) < 0)
exit(1);
// 3. 监听
if (listen(s_socket, 5) < 0)
exit(1);
// 4. 阻塞等待客户端的连接请求
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
int new_sock = accept(s_socket, (struct sockaddr*)&peer, &peer_len);
if (new_sock < 0)
exit(1);
// 5. 连接成功,面向字节流通信
while (1)
{
char buf[1024];
int data_len = read(new_sock, buf, sizeof(buf));
if (data_len == 0)
break;
buf[data_len] = 0;
printf("recv message: %s\n", buf);
// 6. 发送回射信息(应答数据)
char out_buf[1024];
snprintf(out_buf, sizeof(out_buf), "已收到数据: %s\n", buf);
write(new_sock, out_buf, sizeof(out_buf));
}
// 7. 结束连接
close(s_socket);
return 0;
}
client.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int args, char* argv[])
{
if (args != 3)
{
printf("Usage: client server_ip server_port\n");
exit(1);
}
char* s_ip = argv[1];
uint16_t s_port = atoi(argv[2]);
// 1. 创建套接字
int c_socket = socket(AF_INET, SOCK_STREAM, 0);
if (c_socket < 0)
exit(1);
// client其实也需要bind绑定,不过这一步不需显式绑定(由OS随机指定)
// 2. 发送连接请求
struct sockaddr_in server;
socklen_t s_len = sizeof(server);
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(s_ip);
server.sin_port = htons(s_port);
if (connect(c_socket, (struct sockaddr*)&server, s_len) != 0)
{
printf("connect fail\n");
exit(1);
}
// 3. 建立连接成功,面向字节流通信
while (1)
{
char buf[1024];
printf("Enter: ");
gets(buf);
write(c_socket, buf, sizeof(buf));
// 4. 获取服务器的应答数据
char recv_buf[1024];
int data_len = read(c_socket, recv_buf, sizeof(recv_buf));
if (data_len == 0)
break;
recv_buf[data_len] = 0;
printf("%s", recv_buf);
}
// 5. 结束通信
close(c_socket);
return 0;
}
先启动服务器,再启动客户端,运行结果: