[linux] Linux网络之Socket编程入门

Posted 哦哦呵呵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[linux] Linux网络之Socket编程入门相关的知识,希望对你有一定的参考价值。

目录

1. 前言

本文主要是介绍socket编程的一些接口函数,不会涉及协议的特性等等,协议的特性会在接下来的文章中做详细解释,在详解socket编程时,会首先介绍一些网络的基础概念。

2. 网络基础

网络所解决的问题是牵扯到数据双方的,即数据的发送方和接收方。网络解决的就是不同机器之间不同进程的进程间通信问题。在解决进程间通信的问题是,需要考虑数据如何和发送端到达接收端?下文详细介绍。

2.1 协议

所谓协议,一定是两个人的约定,双方按照这种约定就能互相理解对方的行为。即在网络中就是数据发送方和数据接收方的一种约定,数据发送方按照这种约定发送信息,数据接收方根据协议去解读信息。
所以数据在网络中发送时,发送的不仅仅只有数据,还有与之对应的协议。

2.1.1 TCP和UDP协议

这里只会简单介绍两种协议,具体协议内容,将会在下文详细介绍。

TCP:

  • 有连接:通信双方发送数据之前,必须先建立连接,再进行发送
  • 可靠传输:保证数据是可靠且有序的到达对端
  • 面向字节流:多次发送的数据在网络传输过程中没有明显的数据边界

UDP:

  • 无连接:客户端向服务端发送数据时,不需要和服务端先建立连接,而是直接发送数据,客户端也不知道服务端是否在线
    不可靠传输:不会保证数据是可靠且有序到达对端的
    面向数据报:数据不管是和应用层还是和网络层传输,都是整条数据进行交付的

2.2 网络的层状结构

网络层状结构,即层状结构中的每一层实现的功能各不相同,互不影响。一般是下层给上层服务,上层调用下层的接口进行通信,下层将结构发送给上层。

1.OSI七层模型

2.TCP/IP五层模型

  • 应用层: 负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等. 我们的网络编程主要就是针对应用层
  • 传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机.
  • 网络层: 负责地址管理和路由选择. 例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由). 路由器(Router)工作在网路层.
  • 数据链路层: 负责设备之间的数据帧的传送和识别. 例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作. 有以太网、令牌环网, 无线LAN等标准. 交换机(Switch)工作在数据链路层.
  • 物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等. 集线器(Hub)工作在物理层.

每一层都调用它的下一层所提供的接口进行数据的操作

传输层、网络层、数据链路层、物理层在内核中称之为网络协议栈,其中操作系统已经封装好了代码,我们在编程时就是调用所提供给我们的结构进行编程。

2.3 一台主机向另一台主机的发送数据的流向

先看一张图片:

  • 每一层向下交付时,它的全部数据对于下层来说就是有效载荷。
    数据包 = 报头 + 有效载荷,向下或向上交付时都会加上或者去掉有效载荷

数据包的封装和分用

  • 封装:
    应用层数据通过协议栈发送到网络上,每层协议都要加上一个数据首部,上面说的向下交付,要加上该层协议的数据首部,称之为封装
  • 分用:
    数据封装成帧发送到传输介质上,到达目的主机后每层协议在剥离掉响应的首部,根据首部中的上层协议将数据交给对应的上层协议处理
  • 不同的协议层对数据包有不同的称为,传输层叫做段,网络层叫做数据包,链路层叫做帧
  • 首部信息中包含了一些类似于首部有多长,载荷有多长等等信息,会在下文中的协议部分详细介绍

2.4 IP和MAC地址

IP地址:在网络中标识一台主机。是一个32位无符号的整数。使用点分十进制标识
MAC地址:数据链路层相连的设备相互识别,长度48位,6个字节,16进制加冒号表示。

2.5 端口

作用
在一台机器上可以唯一标识一个进程,当网络数据到达网络协议栈之后,可以通过端口信息确认该条数据是发送给哪一个进程的。
注意

  • 一个端口只能被一个进程绑定,一个进程可以绑定多个端口
  • IP + PORT就可以在网络中唯一定位一台主机当中的一个进程

分类
知名端口:0~1023 这些端口已经被一些知名协议或者程序占用了,程序员不能使用
不知名端口:1024~65535,可以由程序员分配使用

2.6 网络字节序

网络数据流也分大小端

  • 发送主机通常将发送缓冲区的数据按找内存地址从低到高的顺序发出
  • 接收主机把从网络上接收到的字节流依次保存在接收缓冲区当中,按内存地址从低到高的顺序保存
  • 网络数据流地址的规定:先发出的数据是低地址,后发出的数据是高地址
  • TCP/IP协议规定:网络数据流采用大端字节序(低地址存放高字节)
    -如果发送方是小端,需要将数据转成大端

网络字节序与主机字节序的转换
接口函数

//作用:将点分十进制的IP字符串转换位uint32_t 的整数
//	   将uint32_t的整数从主机字节序转换为网络字节序
inaddr_t inet_addr(const char *cp);

//作用: 将IP地址从网络字节序转换位主机字节序
//      将uint32_t的整数转换为点分十进制的字符串
inaddr_t inet_ntoa(struct inaddr in);


uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
规则:
	h标识host,n标识network
	l表示32位长整数,s表示16位短整数
	htonl:表示将32位的长整数从著继续转换位网络字节序,例如将IP地址转换后发送

3. Socket API

3.1 公共接口

1. 创建socket文件描述符

int socket(int domain, int type, int protocol);
参数:
	domain: 域,指定网络层所使用的协议
		AF_INET: IPV4协议
		AF_INET6: IPV6协议
		AF_UNIX: 本地域套接字,使用于一台机器的两个进程进行进程间通信
	type: 套接字的类别
		UDP: SOCK_DGRAM
		TCP: SCOK_STREAM
	protocol: 使用的协议
		0: 使用默认协议
		IPPROTO_UDP: 使用udp协议
		IPPORT_TCP: 使用tcp协议
返回值: 返回的是一个文件描述符,套接字描述符

2. 绑定地址信息

int bind(int socket, const struct sockaddr* adderss, socklen_t address_len);
参数:
	socket: 套接字描述符
	address: 地址信息结构
	address_len: 地址信息结构体长度

struct sockaddr是一种通用类型的数据结构,具体采用的是各个协议自己的数据结构,使用时需要强转

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • 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位的整数

3.2 UDP接收发送数据

接收数据:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
参数:
	sockfd:套接字描述符
	buf:将接收的数据存放到buf中
	len:最大的接收长度
	flags:阻塞接收
	src_addr:表示数据从哪一个地址信息来的,即数据从哪一个IP和端口来的
	addrlen: 地址信息长度,输入输出型参数
返回值:
	< 0: 函数调用出错
	= 0: 对端关闭了连接
	> 0: 接收到的字节数量


发送数据:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
			const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
	sockfd:套接字描述符
	buf:将发送的数据
	len:发送数据的长度
	flags:阻塞接收
	dest_addr:当前目的消息的目的地址信息结构,发送到哪里去
	addrlen: 地址信息长度,输入输出型参数
返回值:
	> 0: 发送成功的字节数
	< 0: 发送失败

3.3. TCP部分

1. 开始监听socket

int listen(int socket, int backlog);
参数:
	sock: 侦听套接字,socket函数的返回值
	backlog: 指定内核当中已完成连接的队列大小

backlog 重点解释

  • 概念
    已完成连接队列: 存放已经3次握手完毕,连接已经建立完成的连接
    未完成连接队列: 存放正在处于3次握手过程中的连接,TCP连接还尚未建立完成
  • 结论:
    1.为什么要有backlog?
      为了提高服务器的运行效率,设置一个等待队列,如果服务器请求已经满了,则就会将后来的请求放入等待队列中,等到服务器处理完一个连接后,去通知等待队列中的连接去和服务器建立连接。
    2.为什么不把backlog设置的过长?
      如果设置的过长,势必会减少服务器的资源,设置较短可以节省出资源去处理连接。

2. 服务器接收连接请求

作用: 从内核中的已完成连接队列中接收已经完成三次握手的连接
int accpect(int sock, struct sockaddr* addr, socklen_t* addr_len);
参数:
	sock:侦听套接字
	addr:客户端地址信息结构(出参: 由函数返回客户端的地址信息结构)
	addr_len: 客户端的地址信息长度(出参)
返回值:
	>= 0: 成功,返回一个新连接的套接字,调用这个函数后就会将已完成队列中的连接取出
	< 0:  失败

注意: 当调用accpet函数接收新连接时,如果已完成连接队列中没有新的连接,则accpect函数就会发生阻塞

3. 发送数据

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
	sockfd: 
		客户端: sock函数的返回值
		服务端: accpect函数的返回值
	buf: 待要发送的数据
	len: 发送数据的长度
	flags: 0 阻塞发送  MSG_PEEK 发送紧急数据(带外数据)
返回值:
	-1: 发送失败
	>0: 实际发送出的数量

4. 接收数据

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
	sockfd: 
		客户端: sock函数的返回值
		服务端: accpect函数的返回值
	buf: TCP接收缓冲区的数据
	len: buf的最大长度
	flags: 0 阻塞接收
返回值:
	< 0: 函数调用出错
	= 0: 对端关闭了连接
	> 0: 接收到的字节数量

4. UDP及TCP的通信程序

4.1 UDP

1. UDP编程流程

服务端

1.创建套接字
2.绑定地址信息
3.发送数据,接收数据
4.关闭套接字

客户端

1.创建套接字
2.发送数据,接收数据
4.关闭套接字

注意
客户端不需要进行绑定地址信息,因为客户端可能会有多个程序,进行绑定时会容易造成绑定冲突,应该交由操作系统自动分配

2. 代码

服务端

UDPServer.hpp/

#include <iostream>
#include <string>
#include <map>
#include <cstdio>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

class udpServer

private:
    int port;
    int sock;
    // 使用map实现在线翻译的小功能
    std::map<std::string, std::string> dict;

public:
    udpServer(int _port = 8080)
        : port(_port)
    
        dict.insert(std::pair<std::string, std::string>("apple", "苹果"));
        dict.insert(std::pair<std::string, std::string>("banana", "香蕉"));
        dict.insert(std::pair<std::string, std::string>("student", "学生"));
    

    // 创建upd服务
    // 初始化服务器
    void InitServer()
    
        // 相当于创建了一个文件,并打开 语句执行完后 sock值应当为3,所指的是新创建的文件描述符为3号
        sock = socket(AF_INET, SOCK_DGRAM, 0);
        std::cout << "sock:" << sock << std::endl;

        // bind 绑定自身的网络信息,启动自身,因为服务器是被动相应
        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
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        
            std::cerr << "bind error!\\n" << std::endl;
            exit(1);
        
    

    // 启动服务器  先收数据 再发送数据 简单的回应服务
    void StartServer()
    
        char msg[64];
        for (;;)
        
            msg[0] = '\\0';

            // 对端放松数据的信息
            struct sockaddr_in end_point;
            // 接收数据
            socklen_t len = sizeof(end_point);
            ssize_t s = recvfrom(sock, msg, sizeof(msg) - 1, 0, (struct sockaddr*)&end_point, &len);

            // 数据接收成功
            if (s > 0)
            
                // 获取客户端的ip
                char s_ip[16];
                sprintf(s_ip, "%d", ntohs(end_point.sin_port));
                
                std::string client_ip = inet_ntoa(end_point.sin_addr);
                client_ip += ":";
                client_ip += s_ip;

                msg[s] = '\\0';
                std::cout << client_ip << "# " << msg << std::endl;

                std::string echo_string = msg;
                // echo_string += "[server echo!]";
                
                // 查找map键值对
                echo_string = "unknow";

                auto it = dict.find(msg);
                if (it != dict.end())
                
                    echo_string = dict[msg];
                

                // 将消息回送给客户端
                sendto(sock, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&end_point, len);
            
        
    

    ~udpServer()
    
        close(sock);
    
;

UDPmain.cpp/
#include "udpServer.hpp"

void Usage(std::string proc)

    std::cout << "Usage" << proc << " dict_ip dict_port" << std::endl;


int main(int argc, char* argv[])

    if (argc != 2)
    
        Usage(argv[0]);
        exit(1);
    

    udpServer *up = new udpServer(atoi(argv[1]));
    up->InitServer();
    up->StartServer();

    delete up;

    return 0;

服务端

UDPClient.hpp/

#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

class udpClient

private:
    std::string ip;
    int port;
    int sock;

public:

    // ip port 填入服务器的ip port 
    udpClient(std::string _ip = "127.0.0.1", int _port = 8080)
        : ip(_ip)
        , port(_port)
    

    void InitClient()
    
        // 相当于创建了一个文件,并打开 语句执行完后 sock值应当为3,所指的是新创建的文件描述符为3号
        sock = socket(AF_INET, SOCK_DGRAM, 0);
        std::cout << "sock:" << sock << std::endl;
    

    void StartClient()
    
        std::string msg;
        
        // 发送数据的远端
        struct sockaddr_in peer;
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        peer.sin_addr.s_addr = inet_addr(ip.c_str());

        for (;;)
        
            std::cout << "please Enter# ";
            std::cin >> msg;

            if (msg == "quit")
            
                break;
            

            // 先发送数据
            sendto(sock, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
        
            // 再接收数据
            char echo[128];
            ssize_t s = recvfrom(sock, echo, sizeof(echo) - 1, 0, NULL, NULL);
            if (s > 0)
            
                echo[s] = 0;
                std::cout << "server# " << echo << std::endl;
            
        
    

    ~udpClient()
    
        close(sock);
    
;

UDPMain.cpp/
#include "udpClient.hpp"

void Usage(std::string proc)

    std::cout << "Usage" << proc << " server_ip server_port" << std::endl;


int main(int argc, char* argv[])

    if (argc != 3)
    
        Usage(argv[0]);
        exit(1);
    

    udpClient uc(argv[1], atoi(argv[2]));
    uc.InitClient();
    uc.StartClient();

    return 0;

4.2 TCP

1. 编程流程

服务端编程流程

1.创建流式套接字
2.绑定地址信息
3.监听:告诉操作系统内核可以接收客户端发起的新连接
4.接收新连接
5.断开连接

注意

TCP程序不能使用单进程或单线程!!!
  因为使用单进程程序,一次只能接收一个连接的请求,后续连接就无法接受了。如果将accpect放到while循环中,则每次循环时,只能接收一个新的连接,并且只能通信一次,因为每次接受新连接时就会覆盖掉上次的连接。

TCP程序必须使用多线程或者多进程!!

代码

多线程版本

服务端

TCPServer.hpp/

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>

#define BACKLOG 5

class tcpServer

private:
    int port;
    int lsock;   // 监听套接字

public:
    tcpServer(int _port)
        : port(_port)
     

以上是关于[linux] Linux网络之Socket编程入门的主要内容,如果未能解决你的问题,请参考以下文章

Linux网络之socket编程

linux--网络编程之socket

linux网络编程之socket编程

linux之socket编程总结

linux高性能网络编程读书笔记之socket

linux高性能网络编程读书笔记之socket数据读写