套接字是实现进程间通信的编程。IP可以标定主机在全网的唯一性,端口可以标定进程在主机的唯一性,那么socket通过IP+端口号就可以让两个在全网唯一标定的进程进行通信。
套接字有三种:
域间套接字:实现主机内部的进程通信的编程
原始套接字:使用网络层或者数据链路层的接口进行编程,更难更底层,例如制作抓包等网络工具
网络套接字:实现用户通信的编程
udp网络通信
服务端server
分析:
服务端最少需要有两个接口,一个用来初始化服务器,一个用来运行服务器
一:初始化服务器接口
创建socket,绑定端口
1.创建socket
socket接口

第一个参数是域,用来确定通信类型 
 
AF_UNIX就是域间套接字,AF_INET/AF_INET6是IPv4/IPv6的网络套接字
第二个参数是传输的数据种类

最常用的是前两个,SOCK_STREAM是面向字节流(TCP),SOCK_DGRAM是面向数据报(UDP)
第三个参数传0即可,这里用不到

返回值返回一个文件描述符
2.绑定端口
bind接口

第一个参数是socket返回的文件描述符
第二个参数是一个包含通信属性的结构体,例如通信域,ip地址和端口号。这是一个输出型参数,而且不同的通信类型的结构体不一样,传参时要进行类型转换。


sockaddr_in有三个成员需要我们初始化:
 1.sin_family:通信域,网络通信使用AF_INET
这个成员使用了宏函数,宏传入参数sin_,所以sa_prefix##family等价于sin_family,它是一个无符号短整型
2.sin_port:端口号
端口号会在网络间传输,需要对我们传入的端口号进行处理,使其符合网络字节序,要使用htons()接口
3.sin_addr:ip地址
这个成员是一个结构体,结构体内有一个无符号整形的成员。但是我们一般的ip地址是一个字符串例如(192.168.33.131),所以我们要先将字符串转化为数字,再将数字变为网络字节序。这要用到inet_addr(char*)接口
第三个参数时结构体大小,直接计算即可
注意:一般服务器bind的ip地址是0.0.0.0,如果一个服务是绑定到 0.0.0.0 ,那么外部机器访问该机器上所有 IP 都可以访问该服务。如果服务绑定到的是特定的 ip,则只有访问该 ip 才能访问到服务。
初始化服务器代码样例:
class udpserver{
public:
    void init()
    {
        // 创建udp server
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        {
            exit(1);
        }
        // bind socked
        //初始化结构体
        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);                  // 转化成网络字节序
        local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 将ip转化成数字,再将数字转化为网络字节序
        //bind
        if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            exit(2);
        }
    }
    udpserver(uint16_t port = 8080, string ip = "0.0.0.0")
        : port_(port), ip_(ip)
    {
    }
    ~udpserver()
    {
        if (sockfd_ > 0)
            close(sockfd_);
    }
private:
    int sockfd_;
    uint16_t port_;
    string ip_;
};二:运行服务器接口
接收数据,对数据做处理,最后发送数据
1.接收数据
recvfrom接口


第一个参数是socket返回的文件描述符,第二个是接收的数据存放的位置,第三个参数是buf空间的大小,第四个参数在本文中这里只要传0即可满足需求。
第五个参数是用来存储数据发送方(客户端)属性的结构体,第六个参数是结构体大小的指针
为什么要记录客户端的通信属性呢?因为服务端在对数据做处理后还要发送回客户端。
2.对数据做处理
需要根据实际需求进行处理,可以通过传函数指针,实现代码分层。见后面的代码示例。
3.发送数据
sendto接口


第一个参数是socket的文件描述符,第二个参数是要发送的数据的地址,第三个参数是发送数据的大小,第四个参数传0。
第五个参数是数据接收方(客户端)属性的结构体,第六个参数是结构体大小。
运行服务器代码样例(包括前面的代码):
//udpserver.hpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
using func = function<string(char*)>;
class udpserver{
public:
    void init()
    {
        // 创建udp server
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        {
            exit(1);
        }
        // bind socked
        //初始化结构体
        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);                  // 转化成网络字节序
        local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 将ip转化成数字,再将数字转化为网络字节序
        //bind
        if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            exit(2);
        }
    }
    void run(func fun)
    {
        char buf[1024] = {0};
        while (1)
        {
            struct sockaddr_in client; // 客户端的信息
            socklen_t len = sizeof(client);
            // 收数据
            ssize_t n = recvfrom(sockfd_, buf, sizeof(buf), 0, (struct sockaddr *)&client, &len);
            if (n < 0)
            {
                cout << "receice fail" << endl;
                continue;
            }
            buf[n] = 0;
            cout << "server get:" << buf << endl;
            // 处理数据,通过传递的函数实现数据处理的和网络通信解耦
            string ret = fun(buf);
            
            // 发送数据
            n = sendto(sockfd_, ret.c_str(), ret.size(), 0, (struct sockaddr *)&client, len);
        }
    }
    udpserver(uint16_t port = 8080, string ip = "0,0,0,0")
        : port_(port), ip_(ip)
    {
    }
    ~udpserver()
    {
        if (sockfd_ > 0)
            close(sockfd_);
    }
private:
    int sockfd_;
    uint16_t port_;
    string ip_;
};
//main.cpp
#include"udpserver.hpp"
#include<memory>
using namespace std;
string datagram(char* data)
{
    string ret = "Get message:";
    ret += data;
    return ret;
}
int main()
{
    unique_ptr<udpserver> server(new udpserver());
    server->init();
    server->run(datagram);
    return 0;
}客户端client
分析:
客户端需要初始化客户端,发送数据,接收数据
一:初始化客户端
创建socket和服务端一样,不同的是客户端的bind是操作系统完成的不需要我们操作,客户端会在发送数据时由OS随机bind一个端口。因为客户端的设备可能同时请求多个服务端,例如手机会同时运行很多app,客户端的程序很难为服务端留一个固定的端口(不同公司之间开发软件不可能商量谁要用哪个客户端的端口),而且服务端没必要第一时间知道客户端的端口,客户端发送数据请求时可以获取到客户端的属性
二:发送数据
使用sendto()接口,和服务端一样。
三:接收数据
使用recvfrom()接口,和服务端一样。
客户端代码样例:
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
int main(int argc, char *argv[])
{
    //获取命令行参数,ip和端口号
    if(argc != 3)
    {
        cout << "Please input in this way" << endl;
        cout << "./udpclient" << " [ip]" << " [port]" << endl;
    }
    string ip = argv[1];
    uint16_t port = stoi(argv[2]);
    // 创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        cout << "client create fail" << endl;
    }
    //客户端不需要bind,但是客户端会在发送数据时由OS随机bind一个端口,因为客户端设备同时请求多个服务端,服务端很难让客户端的程序为其留一个固定的端口,而且没必要第一时间知道客户端的端口,客户端发送数据请求时可以获取到
    // 创建服务端信息的结构体
    struct sockaddr_in server;
    server.sin_port = htons(port);
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(ip.c_str());
    socklen_t len = sizeof(server);
    char buf[1024] = {0};
    string message;
    while (1)
    {
        // 发送数据
        cout << "Please Enter@";
        getline(cin, message);
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);
        // 接收数据
        struct sockaddr_in tem;
        socklen_t len_tem = sizeof(tem);
        ssize_t n = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&tem, &len_tem);
        if (n < 0)
        {
            cout << "client receive fail" << endl;
            continue;
        }
        buf[n] = 0;
        cout << buf << endl;
    }
}


















