Linux下Socket编程基础

什么是socket

一、Socket编程概述

Socket是网络通信的端点抽象,如同现实世界中的电话插座,一个人想要用电话,肯定需要把插头插到插座中,而在网络上你想跟其他节点通信,也需要将socket这个插头插进网络中。在Linux系统中,socket表现为一个int类型的文件描述符,我们完全可以把其当作一个文件,对其执行读取写入等操作。

二、从网络模型开始看socket

1. TCP/IP四层模型
层级协议示例核心功能典型设备/实现
应用层HTTP/FTP/SMTP应用程序间通信规范浏览器、邮件客户端
传输层TCP/UDP端到端可靠传输、流量控制、拥塞控制Socket API
网络层IP/ICMP逻辑寻址、路由选择、分组转发路由器
网络接口层Ethernet/Wi-Fi物理传输、MAC地址管理网卡/交换机
2. Socket的跨层特性

socket横跨传输层应用层两个层级

  • 向上,为应用层提供文件描述符的接口
  • 向下,对接TCP/UDP的协议栈

socket编程

一、主机字节序和网络字节序

  • 我们都知道字节序分为大端字节序和小端字节序,而如果我们在两台使用不同字节序的主机之间进行直接通信,那么接收端必定将其错误解释。
  • 解决方法:发送端将数据转化为大端字节序然后发送,然后接收端根据自身采用的字节序来判断是否需要对数据进行转换,因此大端字节序也被称为网络字节序号。
  • linux提供的四个字节序转换函数,函数最后的s和l分别代表short和long:
函数名功能描述示例
htons()16位主机字节序转网络字节序8888 → 0x22B8
ntohs()16位网络字节序转主机字节序0x22B8 → 8888
htonl()32位主机字节序转网络字节序8888 → 0x22B8
ntohl()32位网络字节序转主机字节序0x22B8 → 8888
  • 另外还有两个ip地址转换函数
函数名功能描述示例
inet_addr()点分十进制转32位网络字节序“127.0.0.1” → 0x7f000001
inet_ntoa()32位网络字节序转点分十进制0x7f000001 → “127.0.0.1”

二、核心流程对比

服务器端步骤客户端步骤
1. 创建socket1. 创建socket
2. 绑定地址和端口2. 设置服务器地址
3. 监听端口3. 发起连接请求
4. 接受客户端连接
5. 进行数据通信4. 进行数据通信

三、关键函数与代码解析

1. 创建Socket

这一步的目的是创建socket的文件描述符,相当于创建文件,但是文件的路径等其他信息我们并没有指定。

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  • 参数解析
    • AF_INET: IPv4地址族(32位地址+16位端口),常用可选参数还有AF_INET6(IPv6协议族)
    • SOCK_STREAM: 面向连接的可靠传输(TCP),常用可选参数还有SOCK_DGRAM,这表示传输层使用UDP协议。
    • 0: 自动选择协议(TCP/UDP),即根据前面两个参数自动确定。
  • 返回值:成功返回≥3的整数(0/1/2被标准输入输出占用)
2. 地址结构初始化

创建完文件描述符后,我们需要初始化socket的地址结构,也就是表明文件的路径。因为通用结构体并不好用,比如获取ip地址和端口号就要进行繁琐的位操作,这里我们使用的socket地址结构是IPv4专用结构体sockaddr_in。

struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr));  // 清空结构体
serv_addr.sin_family = AF_INET;        // 地址族
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // IP转换
serv_addr.sin_port = htons(8888);      // 端口转换
  • sockaddr_in结构体
struct sockaddr_in {
    sa_family_t    sin_family;  // 地址族(2字节)
    in_port_t      sin_port;    // 端口号(2字节)
    struct in_addr sin_addr;    // IPv4地址(4字节)
};

struct in_addr {
    uint32_t s_addr; // 网络字节序的32位地址
};
  • 字节序转换
    • htons(): Host to Network Short(16位端口)
    • inet_addr(): 点分十进制转32位网络字节序
  • 参数解析
    • AF_INET:指定结构体的地址族,因为我们使用的是IPv4专用socket地址,故而这里只能填AF_INET
    • inet_addr("127.0.0.1"):将点分十进制转化为网络字节序。赋值ip地址,否则通信另一方无法发现主机。
    • htons(8888):将主机字节序转换为网络字节序。赋值端口,否则通信另一方无法找到通信程序。
3. 绑定地址

将文件描述符和socket地址进行绑定。

bind(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr));
  • 参数解析
    • sockfd:需要绑定的文件描述符。
    • (sockaddr*)&serv_addrsockaddr_in(专用)转sockaddr(通用),需要绑定的socket地址。
    • sizeof(serv_addr):socket地址长度。
4. 监听端口
listen(sockfd, SOMAXCONN);
  • 参数解析
    • sockfd:需要监听的文件描述符。
    • SOMAXCONN:表示已完成连接队列的最大长度,一般直接用SOMAXCONN即系统定义的最大值
5. 接受连接

当listen监听到客户端想要connect的请求时,可以用accept函数接受这个连接,即获得客户端文件描述符。

struct sockaddr_in clnt_addr; //存储接收到的客户端的socket地址
socklen_t clnt_addr_len = sizeof(clnt_addr);// 计算客户端地址的长度,方便传入指针。
int clnt_sockfd = accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len);//接受客户端文件描述符,以及将其和客户端socket地址绑定。
  • 差异:需要注意的是accept函数和bind函数中的参数类型基本一致,只是第三个参数需要传递指针。
  • socklen_t
    • 本质typedef unsigned int socklen_t
    • 作用:表示socket地址结构的长度
    • 特殊行为
      • 在accept()中作为值-结果参数(value-result argument)
      • 输入时指定缓冲区大小,输出时返回实际地址长度
6. 客户端连接

运行在客户端上的函数,connect服务端上的listen函数,参数和bind函数并无不同

connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr));

三、代码示例

请注意,因为是示例代码,我并没有加入错误报告函数,但是这在socket编程中十分重要。

服务端代码 (server.cpp):

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUF_SIZE 1024

using namespace std;

int main() {
    int serverSocket, clientSocket;
    sockaddr_in serverAddr, clientAddr;
    socklen_t clientAddrLen = sizeof(clientAddr);
    char buffer[BUF_SIZE];

    // 创建socket
    serverSocket = socket(AF_INET, SOCK_STREAM, 0);

    // 配置服务器地址
    serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = inet_addr(SERVER_IP); 
    ser_addr.sin_port = htons(PORT);

    // 绑定socket
    bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));

    // 开始监听
    listen(serverSocket, SOMAXCONN);

    cout << "Server listening on port " << PORT << "..." << endl;

    // 接受客户端连接
    clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrLen);

    cout << "Client connected!" << endl;

    // 接收客户端数据
    int bytesRead;
    while ((bytesRead = recv(clientSocket, buffer, BUF_SIZE, 0)) > 0) {
        buffer[bytesRead] = '\0'; // Null-terminate the received data
        cout << "Received from client: " << buffer << endl;

        // 发送回应
        send(clientSocket, "Message received", 17, 0);
    }

    // 关闭socket
    close(clientSocket);
    close(serverSocket);

    return 0;
}

客户端代码 (client.cpp):

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUF_SIZE 1024

using namespace std;

int main() {
    int clientSocket;
    sockaddr_in serverAddr;
    char buffer[BUF_SIZE];

    // 创建socket
    clientSocket = socket(AF_INET, SOCK_STREAM, 0);

    // 配置服务器地址
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
    serverAddr.sin_port = htons(PORT);

    // 连接到服务器
    connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));

    cout << "Connected to server!" << endl;

    // 发送数据到服务器
    string message = "Hello, Server!";
    send(clientSocket, message.c_str(), message.length(), 0);

    // 接收服务器的回应
    int bytesReceived = recv(clientSocket, buffer, BUF_SIZE, 0);
    if (bytesReceived > 0) {
        buffer[bytesReceived] = '\0'; // Null-terminate the received data
        cout << "Received from server: " << buffer << endl;
    }

    // 关闭socket
    close(clientSocket);

    return 0;
}
编译与运行:
  1. 编译代码:

    g++ server.cpp -o server
    g++ client.cpp -o client
    
  2. 运行服务端:

    ./server
    
  3. 运行客户端:

    ./client
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值