Linux网络编程套接字

Posted 小倪同学 -_-

tags:

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

文章目录

预备知识

理解源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址

源IP地址: 表示该条信息来源于哪个机器。
目的IP地址: 表示该条信息去往于哪个进程

理解 “端口号” 和 “进程ID”

我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?

端口号(port)是传输层协议的内容

  • 端口号是一个2字节16位的整数
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
  • 一个端口号只能被一个进程占用
  • 一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定

理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”

源端口号: 表示该条信息来源于哪个进程。
目的端口号: 表示该条信息去往于哪个机器。

认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识,后面再详细讨论

  • 传输层协议
  • 有连接: 双方在发送网络数据之前必须建立连接,再进行发送
  • 可靠传输: 保证数据是可靠并且有序的到达对端,例如发送123、456时123数据先到达,456数据后到达。即使456数据先到达传输层,也会阻塞等待前面的数据123先到达。
  • 面向字节流: TCP发送数据的单位是以字节为单位,并且数据没有明显的边界例如:123456数据不会分开

认识UDP协议

这里我们也先对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;,后面再详细讨论

  • 传输层协议
  • 无连接: 双方在发送网络数据之前不需要建立连接,直接发送,客服端不用管服务端是否在线
  • 可靠传输: UDP并不会保证数据有序的到达对端
  • 面向字节流: UDP不管向应用层还是网络层传递数据都是整条数据

网络字节序

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

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

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

#include	<arpa/inet.h>

uint16_t htons(uint16_t hostshort)
uint32_t htonl(uint32_t hostlong)

uint16_t ntohs(uint16_t netshort)
uint32_t ntohl(uint32_t netlong)

这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

UDP协议使用

socket编程接口

  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);
  1. sockaddr结构

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

  • 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结构体指针做为参数。
  1. sockaddr 结构

  1. sockaddr_in 结构

    虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。

  2. in_addr结构


in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。

简单的UDP网络程序

下面实现一个简单的网络通信程序

客户端文件 udp_client.cc

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

// ./udp_client desc_ip desc_port
// ./udp_client 42.123.43.123 8080
void Usage(std::string proc)

    std::cerr << "Usage: " << "\\n\\t" << proc << " desc_ip desc_port" << std::endl;


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

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

        int sock=socket(AF_INET,SOCK_DGRAM,0);
        if(sock<0)
                std::cerr<<"socket error"<<std::endl;
                return 2;
        

        //bind,client端,不需要明确bind,原因??
        //需不需要bind??需要
        //不需要用户去主动bind,实际上,在sendto的时候,OS会自动随机给client bind端口号

        char buffer[1024];

        struct sockaddr_in desc;
        memset(&desc,0,sizeof(desc));
        desc.sin_family=AF_INET;
        desc.sin_port = htons(atoi(argv[2]));
        desc.sin_addr.s_addr = inet_addr(argv[1]);

        for( ; ; )
                std::cout<<"Please Enter# "<< std::endl;
                fflush(stdout);
                buffer[0]=0;
                ssize_t size=read(0,buffer,sizeof(buffer)-1);
                if(size>0)
                        buffer[size-1]=0;
                        //std::cout<<"echo# "<<buffer<<std::endl;
                        sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&desc/*发送到哪里*/,sizeof(desc)/*长度*/);

                        struct sockaddr_in peer;
                        socklen_t len=sizeof(peer);
                        ssize_t s=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);//peer,len 暂时不用
                        if(s>0)
                                buffer[s]=0;
                                std::cout<<"#echo "<<buffer<<std::endl;
                        
                
        

        close(sock);
        return 0;

服务端文件 udp_server.cc

#include<iostream>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<time.h>

#define PORT 8081

void Usage(std::string proc)

    std::cerr << "Usage: " << "\\n\\t" << proc << " local_port" << std::endl;

// ./udp_server port
int main(int argc,char *argv[])

        if(argc!=2)
                Usage(argv[0]);
                return 1;
        
        int sock=socket(AF_INET, SOCK_DGRAM, 0);
        if(sock<0)
                std::cerr<<"socket error"<<std::endl;
                return 2;
           
        std::cout<<"sock: "<<sock<<std::endl;

        //该结构是OS给你提供的一个结构体,用户层定义的,local是属于main函数内的一个临时变量
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(atoi(argv[1])); //后续网络端口,会以源端口的方式,发送给对面
        //注意: 云服务器你要bind的时候的,一般不能直接绑定明确的IP,INADDR_ANY
        //非常推荐使用INADDR_ANY, bind所有你的机器上面的ip
        local.sin_addr.s_addr = htonl(INADDR_ANY); //一个IP本质,可以使用4个字节进行保存[0-255].[0-255].[0-255].[0-255], "42.192.83.143",点分十进制字符串风格IP

        if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)//就是将本主机相关的ip,端口,协议家族等信息写入到特定的fd标定的文件中
                std::cerr<<"bind error"<<std::endl;
                return 1;
        

        char message[1024];
        for( ; ; )
                struct sockaddr_in peer;
                socklen_t len=sizeof(peer);
                ssize_t s=recvfrom(sock,message,sizeof(message)-1,0,(struct sockaddr*)&peer,&len);
                if(s>0)
                        message[s]='\\0';
                        std::cout << "client# " << message << std::endl;
                        std::string echo_message = message;
                        echo_message += "_server_";
                        echo_message += std::to_string((long long)time(nullptr));

                        //std::cout<<"client# " << message << std::endl;
                        sendto(sock,echo_message.c_str(),echo_message.size(),0,(struct sockaddr*)&peer,len);
                
                else

                
        

        close(sock);
        return 0;


关于绑定的一些问题

Server端,为何要明确bind ?
client:server = n:1, server给别人提供服务,就需要自己尽可能的将自己暴露出去(IP(域名)+PORT(一般是被隐藏的)),必须是“稳定”(不能轻易改变,尤其是端口号)的。

client为何不需要明确bind?
如果client没有port,也变无法与server进行通信。
为何不需要我们给他bind呢?
如果你自己bind了,成功了还好,如果你的client端口被别的程序占用,你的client就无法启动,客户端不是一定要用哪一个端口,只需要有一个端口就可以。我们一般不自己bind,而是由OS随机帮我们查找端口.

TCP协议使用

TCP socket API

下面介绍程序中用到的socket API,这些函数都在sys/socket.h中

socket()

  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符
  • 应用程序可以像读写文件一样用read/write在网络上收发数据
  • 如果socket()调用出错则返回-1
  • 对于IPv4, family参数指定为AF_INET
  • 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
  • protocol参数的介绍从略,指定为0即可

bind()

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号
  • bind()成功返回0,失败返回-1
  • bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号
  • 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度

我们的程序中对myaddr参数是这样初始化的

bzero ( &servaddr , sizeof ( servaddr ) ) ;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s _addr= hton1 (INADDR_ANY ) ;
servaddr.sin port = htons ( SERV_PORT);
  1. 将整个结构体清零
  2. 设置地址类型为AF_INET
  3. 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址
  4. 端口号为SERV_PORT

listen()

  • listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5), 具体细节同学们课后深入研究
  • listen()成功返回0,失败返回-1

accept()

  • 三次握手完成后, 服务器调用accept()接受连接
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号
  • 如果给addr 参数传NULL,表示不关心客户端的地址
  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)

服务器程序结构是这样的

while ( 1 )
	cliaddr_len = sizeof( cliaddr ) ;
	connfd =accept ( listenfd,(struct sockaddr * ) &cliaddr , &cliaddr_len ) ;
	n = read ( connfd, buf,MAXLINE);
	...
	close( connfd ) ;

connect

  • 客户端需要调用connect()连接服务器
  • connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址
  • connect()成功返回0,出错返回-1

查看tcp相关信息可以用如下指令 netstat -nltp

  1. n能显示数字
  2. l只查看listen状态的接口
  3. t查看tcp链接
  4. p查看到与tcp服务相关的进程信息

简单TCP网络程序

下面编写TCP网络程序实现通信

handler.hpp文件

#pragma once

#include"tcp_server.hpp"

namespace ns_handler
        using namespace ns_tcpserver;

#define SIZE 1024

        void HandlerHelper(int sock)
           
                while(true)
                        char buffer[1024];
                        ssize_t s=read(sock,buffer,sizeof(buffer)-1);
                        if(s>0)
                                // read success
                                buffer[s]=0;
                                std::cout<<"clinet# "<<buffer<<std::endl;
                                std::string echo_string =buffer;

                                if(echo_string=="quit")
                                        break;
                                   

                                echo_string +="[server say]";
    
                                write(sock,echo_string.c_str(),echo_string.size());

                            
                        else if(s==0)
                                //对端链接关闭
                                std::cout << sock << " : client quit ..." << std::endl;
                                break;

                           
                        else
                                // 读取失败
                                std::cerr << "read error" << std::endl;
                                break;
                        
                
        
        
        void HandlerSock_V1(int sock) 
        
    
            HandlerHelper(sock); 
        


tcp_client.hpp文件

#pragma once

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

namespace ns_tcpclient
        class TcpClient
                private:
                        std::string desc_ip;// client要访问的对端服务器的IP地址
                        uint16_t desc_port;//client要访问的对端服务器的port端口号
                        int sock;
                public:
                        TcpClient(std::string _ip,uint16_t _port):desc_ip(_ip),desc_port(_port),sock(-1)
                        

                        void InitTcpClient()
                        
                                //创建socket
                                sock=socket(AF_INET,SOCK_STREAM,0);
                                if(sock<0)
                                        std::cerr<<"socket error"<<std::endl;
                                        exit(2);
                                
                                //2. client要不要bind??不要自己进行bind!在你发起链接的时候,OS会自动给你进行相关的绑定!
                                //3. client要不要listen?不需要!
                                //4. client要不要accept?不需要!

                        

                        //tcp是面向连接的!client 要通信之前必须先连接

                        void Start()
                        
                                //填充对方服务器的socket信息
                                struct sockaddr_in svr;
                                bzero(&svr,sizeof(svr));
                                svr.sin_family= AF_INET;
                                svr.sin_port = htons(desc_port);
                                svr.sin_addr.s_addr = inet_addr(desc_ip.c_str());

                                // 发起链接请求
                                if(connect(sock,(struct sockaddr*)&svr,sizeof(svr))==0)
                                        std线程同步与异步套接字编程

Linux系统编程5_条件变量与互斥锁

Linux系统编程5_条件变量与互斥锁

linux网络编程-posix条件变量(40)

Linux:获取孙子的 pid C++

线程与进程