【网络编程】揭开套接字的神秘面纱(一)

简介: 【网络编程】揭开套接字的神秘面纱(一)

1 🍑简单理解TCP/UDP协议 🍑

TCP协议:

  • 1️⃣传输层协议
  • 2️⃣有连接
  • 3️⃣可靠传输
  • 4️⃣面向字节流

UDP协议:

  • 1️⃣传输层协议
  • 2️⃣无连接
  • 3️⃣不可靠传输
  • 4️⃣面向数据报

2 🍑网络字节序 🍑

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

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

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

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

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

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

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

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

       #include <arpa/inet.h>
       uint32_t htonl(uint32_t hostlong);
       uint16_t htons(uint16_t hostshort);
       uint32_t ntohl(uint32_t netlong);
       uint16_t ntohs(uint16_t netshort);

3 🍑socket编程接口 🍑

3.1 🍎socket 常见API 🍎

// 创建 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);

3.2 🍎sockaddr结构🍎

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

35e59065c48140f6a0a369feba0d4a0e.png

所以当我们使用的时候可以将地址强转成 sockaddr* 类型。

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

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

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

struct sockaddr的定义:

3b74d58ef9b744de92369bca05cc85d4.png

struct sockaddr_in的定义:


37536315b6f9483489f2b888f994cd96.png

4 🍑简单的UDP网络程序 🍑

4.1 🍎基本分析🍎

在写之前,我们先来简单的分析分析下我们应该怎样写?首先我们封装一个udpServer的类来帮助我们创建套接字以及套接字的初始换工作,当然客户端也可以使用这种方式来完,不过由于客户端的代码很简单,我就不在封装一个udpClient的类了。

其次我们思考下udpServer类中成员应该有哪些?

首先肯定要一个套接字(其本质就是一个文件描述符),其次我们需要一个端口号,大家猜一下,我们需要一个IP地址吗?这个其实是不需要的,因为一款服务器/云服务器一般是不要指定某一个具体的IP地址的.

那我们bind的时候应该怎样传入参数呢?这个大家先不急,等会儿将代码写好了大家在回过来看就会清晰很多。为了方便使用我们还可以用一个包装器来包装我们将来要执行回调的函数。

4.2 🍎udpServer.hpp(重点)🍎

#pragma once 
#include<iostream>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<cstring>
#include<string>
#include<functional>
using namespace std;
using fun_t =function<string(string)>;
class udpServer
{
public:
    const static uint16_t defaultPort=8848;
    udpServer(fun_t service=nullptr, uint16_t port =defaultPort)
    :_service(service)
    ,_port(port)
    {}
    void init()
    {
        //1 创建套接字,打开网络文件
        _socket=socket(AF_INET,SOCK_DGRAM,0);
        if(_socket<0)
        {
            cerr<<"create socket fail"<<endl;
            exit(-1);
        }
        //2 bind
        sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family=AF_INET;
        local.sin_port=htons(_port);
        local.sin_addr.s_addr=INADDR_ANY;
        if(bind(_socket,(sockaddr*)&local,sizeof(local))<0)
        {
            cerr<<"bind fail"<<endl;
            exit(-2);
        }
        cout<<"bind success"<<endl;
    }
    void start()
    {
        char buffer[1024];//自定义缓冲区
        while(true)
        {
            //1 从客户端收消息
            sockaddr_in client;//用作输出型参数,用来接受是哪个具体的客户端发送数据给服务端的
            socklen_t len=sizeof(client);
            int n=recvfrom(_socket,buffer,sizeof(buffer)-1,0,(sockaddr*)&client,&len);
            if(n>0)
                buffer[n]=0;
            else
                continue;
           // cout<<"receive message success"<<endl;
           string clientIp=inet_ntoa(client.sin_addr);
           uint16_t clientPort=ntohs(client.sin_port);
           cout<<clientIp<<"-"<<clientPort<<":"<<buffer<<endl;
            //2 处理消息
            string message=_service(buffer);
            //3 发送消息给客户端
            if(sendto(_socket,message.c_str(),message.size(),0,(sockaddr*)&client,sizeof(client))<0)
            {
                cerr<<"send message fail"<<endl;
                exit(-3);
            }
            //cout<<"send message success"<<endl;
        }
    }
private:
    int _socket;
    uint32_t _port;
    fun_t _service;
};

4.2.1 🍋注意事项🍋

  • 1️⃣ 创建套接字所要的头文件是:
 #include <sys/types.h>          /* See NOTES */
 #include <sys/socket.h>

但是sockaddr_in是定义在下面的头文件中的:

#include <netinet/in.h>
#include <arpa/inet.h>

所以我们写套接字编程的时候,这四个头文件都要带上。

  • 2️⃣由于我们使用的是udp协议,所以我们使用的是SOCK_DGRAM,如果是tcp协议,我们使用的是SOCK_STREAM

1c8efef2053244849cc4b1e8192f24d9.png

  • 至于第三个参数默认给0即可。
  • 3️⃣在bind的时候我们由于类中成员并没有加上IP地址,所以我们使用下面这种写法:

c05430e5c21e40e29acfad2beae2fb2f.png


4.3 🍎udpClient.cc🍎

#include"udpServer.hpp"
//./udpClient serverIp serverPort
void usage()
{
    cout<<"Usage error\n\t"<<"serverIp serverPort"<<endl;
    exit(-1);
}
int main(int argc,char*args [])
{
    if(argc!=3)
    {
        usage();
    }
    string serverIp=args[1];
    uint16_t serverPort=stoi(args[2]);
    //1 创建套接字
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if(sock<0)
    {
        cout<<"create socket fail"<<endl;
        exit(-1);
    }
    //2 client要不要bind呢?要不要自己bind呢?
    //要bind 但是不要自己bind 操作系统会帮助我们做这件事情
    // 2 明确server
    sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(serverPort);
    server.sin_addr.s_addr=inet_addr(serverIp.c_str());
    while(true)
    {
        //1 用户输入
        string message;
        cout<<"[grm]:";
        getline(cin,message);
        sendto(sock,message.c_str(),message.size(),0,(sockaddr*)&server,sizeof(server));
        //2 接受服务端信息
        char buffer[1024];
        sockaddr_in tmp;
        socklen_t len=sizeof(tmp);
        int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(sockaddr*)&tmp,&len);
        if(n>0)
        {
            buffer[n]=0;
            cout<<buffer<<endl;
        }
    }
    return 0;
}

4.3.1 🍋注意事项🍋

1️⃣在客户端这里,我们不难发现我们是没有自己手动bind的,为什么呢?

在这之前我们先要明确一点,就是客户端也是必须要bind的,这件事只不过是操作系统帮助我们做了。但是大家肯定又有一个疑问:为什么服务端我们要自己手动bind呀?

server的端口号要我们自己bind是因为服务器的端口号是众所周知的,且不能够随意改变;客户端不需要我们手动bind是因为害怕我们自己bind端口号时会发生冲突,所以这件事就交给了操作系统来帮助我们做。

2️⃣在明确服务端的时候我们使用了下面的接口函数:

a1191d2f295542039e4af0949430eea6.png

这个函数有两个作用:

  1. 将字符串类型转化成四字节的uint32_t类型的四字节整数;
  2. 将主机序列转化成网络序列。

与这个函数具有同种功能的函数还有inet_aton


d782f0e46de242cc986bb87b507f8c1a.png

而上面的inet_ntoa则是与inet_aton具有相反的功能。

除此之外,还有inet_ptoninet_ntop:


e2227e77a6dd4694bff403175ba7292c.png


335d15aa4717499cae8f8f8e96eadd47.png

在这个系列的转换函数中不仅可以转换IPV4的地址,也可以转换IPV6的地址。

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?

44c3ee3067564711a0ebeb6073811f43.png

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.

那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

6ea77379aa474f5383429c372364e683.png

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。

如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?在APUE中, 明确提出inet_ntoa不是线程安全的函数;但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。


目录
相关文章
|
2月前
|
Java
[Java]Socket套接字(网络编程入门)
本文介绍了基于Java Socket实现的一对一和多对多聊天模式。一对一模式通过Server和Client类实现简单的消息收发;多对多模式则通过Server类维护客户端集合,并使用多线程实现实时消息广播。文章旨在帮助读者理解Socket的基本原理和应用。
33 1
|
3月前
|
网络协议 算法 网络性能优化
C语言 网络编程(十五)套接字选项设置
`setsockopt()`函数用于设置套接字选项,如重复使用地址(`SO_REUSEADDR`)、端口(`SO_REUSEPORT`)及超时时间(`SO_RCVTIMEO`)。其参数包括套接字描述符、协议级别、选项名称、选项值及其长度。成功返回0,失败返回-1并设置`errno`。示例展示了如何创建TCP服务器并设置相关选项。配套的`getsockopt()`函数用于获取这些选项的值。
|
3月前
|
网络协议
关于套接字socket的网络通信。&聊天系统 聊天软件
关于套接字socket的网络通信。&聊天系统 聊天软件
|
4月前
|
网络协议 Java
一文讲明TCP网络编程、Socket套接字的讲解使用、网络编程案例
这篇文章全面讲解了基于Socket的TCP网络编程,包括Socket基本概念、TCP编程步骤、客户端和服务端的通信过程,并通过具体代码示例展示了客户端与服务端之间的数据通信。同时,还提供了多个案例分析,如客户端发送信息给服务端、客户端发送文件给服务端以及服务端保存文件并返回确认信息给客户端的场景。
一文讲明TCP网络编程、Socket套接字的讲解使用、网络编程案例
|
6月前
|
网络协议 Java API
网络编程套接字(4)——Java套接字(TCP协议)
网络编程套接字(4)——Java套接字(TCP协议)
59 0
|
6月前
|
Java 程序员 Linux
网络编程套接字(3)——Java数据报套接字(UDP协议)
网络编程套接字(3)——Java数据报套接字(UDP协议)
54 0
|
6月前
|
网络协议 API
网络编程套接字(2)——Socket套接字
网络编程套接字(2)——Socket套接字
38 0
|
6月前
网络编程套接字(1)—网络编程基础
网络编程套接字(1)—网络编程基础
34 0
|
5月前
|
网络协议 安全 Java
Java中的网络编程:Socket编程详解
Java中的网络编程:Socket编程详解
|
5月前
|
网络协议 安全 Java
Java中的网络编程:Socket编程详解
Java中的网络编程:Socket编程详解