Linux----网络编程socket

Posted 4nc414g0n

tags:

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

网络编程socket

1)端口号

src:ip + src:port <-> dst:ip + dst:port (确定互联网中唯一程序 <-> 确定互联网中唯一程序)


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

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

"端口号" 和 "进程ID":

  • 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定

源端口号和目的端口号:

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

2)初识TCP/UDP协议

TCP:

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

UDP:

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

3)网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分
定义网络数据流的地址:

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

为使网络程序具有可移植性使同样的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);
功能:

  1. htonl() 函数将无符号整数 hostlong 从主机字节顺序转换为网络字节顺序。
  2. htons() 函数将无符号短整数 hostshort 从主机字节顺序转换为网络字节顺序。
  3. ntohl() 函数将无符号整数 netlong 从网络字节顺序转换为主机字节顺序。
  4. ntohs() 函数将无符号短整数 netshort 从网络字节顺序转换为主机字节顺序。

注意:

  1. h表示host,n表示network,l表示32位长整数,s表示16位短整数
  2. 在 i386 上,主机字节顺序是最低有效字节在前,而在 Internet 上使用的网络字节顺序是最高有效字节在前
  3. 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回

4)socket编程

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

①sockaddr结构

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6(AF_xxx为地址族). 这样,只要取得某种sockaddr结构体的首地址,
  • 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数

sockaddr结构体:

  • sa_family_t是 unsigned short即8bit (sockaddr通常作为"模板",一般的编程中并不直接对此数据结构进行操作,而使用另一个与之等价的数据结构sockaddr_in)

sockaddr_in 结构体:

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

in_addr结构体:

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

sockaddr_un结构体:

  • 进程间通信的一种方式是使用UNIX套接字,人们在使用这种方式时往往用的不是网络套接字,而是一种称为本地套接字的方式。这样做可以避免为黑客留下后门

②socket接口


注意:我们编写的均是用户层代码

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
int socket(int domain, int type, int protocol);
int close(int fd);


功能:创建socket文件描述符(TCP/UDP, 客户端 + 服务器),close()关闭socket


参数:

  1. domin:即协议家族,一般是AF_INET(表明底层使用IPV4协议)
    协议家族在<sys/socket.h>中定义
  2. type:套接字种类,它指定了通信语义, 常用的是SOCK_STRAM(TCP) 和 SOCK_DGRAM(UDP)其他的有:
  3. protocol:参数默认设为0(通常只存在一个协议来支持给定协议族中的特定套接字类型,指定为0)

返回值:成功时,返回新套接字的文件描述符。 出错时,返回 -1,并设置 errno

#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, socklen_t address_len);


功能:绑定端口号 (TCP/UDP, 服务器)


参数:

  1. socket:socket函数返回的值,即文件描述符
  2. address:(一个结构体 struct sockaddr),实际上我们用的是sockaddr_in 并传入参数的时候强转为(struct sockaddr*)
  3. address_len:address指向的结构体大小字节数

返回值:成功0,失败-1

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);


功能:从套接字接收消息(for udp)


参数:

  1. sockfd:文件描述符
  2. buf:接收msg的缓冲区
  3. len:缓冲区大小
  4. flags:默认设置为0,阻塞方式
  5. src_addr和addrlen:输入输出型参数,获取对端的socket信息的缓冲区,长度缓冲区

返回值:成功返回收到多少字节,失败返回-1

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);


功能:发送消息到另一个套接字(for udp)


参数:

  • 同 recvfrom()

返回值:成功返回传出多少字节,失败返回-1


#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);


功能:建立监听,能否建立需要accept函数去进行检查(for tcp)


参数:

  1. sockfd:文件描述符
  2. backlog:指定最多允许多少个客户连接到服务器。它的值至少为1。收到连接请求后,这些请求需要排队,如果队列满,就拒绝请求

返回值:成功返回0,失败返回-1

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);


功能:接受一个客户端的连接请求,并返回一个新的套接字(for tcp)


参数:

  1. sockfd:文件描述符
  2. addr addrlen:同bind()

返回值:成功返回一个非负整数(不同的客户端的socket对象和属于客户端的套接字),失败返回-1

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);


功能:发出连接请求 (自动检查是否绑定端口,若没有绑定,则它会自动绑定一个本地端口)(for tcp)


参数:

  1. sockfd:文件描述符
  2. addr addrlen:同bind()

返回值:成功返回0,失败返回-1


③UDP示例代码及其注意点


基于网络的三子棋(MARK一下)


udp_server.cc

#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <time.h>//获取时间戳to_string((long long)time(nullptr));
#include <iostream>
#include <cstring>//bzero
#include <netinet/in.h>//sockaddr结构体成员使用时需要引入
#include <string.h> using namespace std;

void Usage(string proc)

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

int main(int args,char* argv[])//命令行参数接收IP和端口 

        if(args != 2)
                Usage(argv[0]);
                return 1;//中断
        
        int sock=socket(AF_INET, SOCK_DGRAM, 0);
        if(sock<0)
                cerr<<"socket error"<<endl;
                return 2;
        
        cout<<"sock return the fd is: "<<socket<<endl;//一定为3
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));//也可使用bzero()
        local.sin_family=AF_INET;
        local.sin_port = htons(atoi(argv[1]));//注意先char转int 再 主机转网络,后续网络端口,会以源端口的方式发送给对面
        local.sin_addr.s_addr = htonl(INADDR_ANY);//主机转网络 一个IP的本质可以使用4个字节进行保存[0-255].[0-255].[0-255].[0-255],"42.165.65.134"是点分十进制字符串风格的IP
        if(bind(sock, (struct sockaddr*)&local,sizeof(local))<0)//C++中可以直接sockaddr表示结构体类型
                cerr<<"bind error"<<endl;
                return 3;
        
        char message[1024];
        for(;;)
                memset(message, 0, sizeof(message));
                struct sockaddr_in peer;
                socklen_t len=sizeof(peer);
                ssize_t s=recvfrom(sock, message ,sizeof(message)-1,0,(struct sockaddr*)&peer,&len);//s是收到的内容字节长度
                if(s>0)
                        //command运行命令
                        char *command[64] =0;
                        char *str = message;
                        command[0] = strsep(&str, " ");
                        int i=1;
                        while((command[i] = strsep(&str, " ")) && command[i]!= NULL)
                              i++;
                        
                        command[i+1]=NULL;
                        if(fork()==0)//子进程进行程序替换
                              execvp(command[0],command);
                              cerr<<"client message# ";
                              for(int j=0;j<i;j++)
                                      printf("%s ",command[j]);
                              
                              cout<<endl;
                              exit(4);
                        
                        sendto(sock,command[0],strlen(command[0]),0, (struct sockaddr*)&peer,len);
                
                else
                        //TODO
        
        close(sock);
        return 0; 

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


注意:

  1. 一个IP的本质可以使用4个字节进行保存[0-255].[0-255].[0-255].[0-255], "42.165.65.134"是点分十进制字符串风格的IP
    使用库函数:int addr_t(const char*cp)可以将分十进制字符串风格的IP转为机器识别的4字节IP
  2. INADDR_ANY:云服务器在bind识,一般不能直接绑定明确的IP,直接使用INADDR_ANY,绑定所有机器上的IP(可能两张网卡),注意:不推荐直接绑定确定的ip
  3. 传入参数的时候注意强转为(struct sockaddr*)
  4. memset可以替换为bzero
  5. 注意调用网络字节序和主机字节序的转换函数 htonl htons和字符点分十进制转二进制函数inet_addr()等
  6. 摘自Linux内核2.6.29,strtok函数已经不再使用,由速度更快的strsep代替
  • char *strsep(char **stringp, const char *delim);
  • stringp:注意是二级指针(), delim:分隔符
  • 模板:while((p = strsep(&str, " ")) != NULL)

udp_client.cc

#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>//点分十进制转二进制
#include <arpa/inet.h>//主机转网络接口等,点分十进制转二进制 using namespace std;
void Usage(string proc) 
        cerr<<"Usage :"<<"\\n\\t"<<proc<< " dest_ip dest_port"<<endl;
        //return 1;  int main(int args, char* argv[]) 
        if(args != 3)//程序名 目的ip 目的端口
                Usage(argv[0]);
                return 1;
        
        int sock=socket(AF_INET, SOCK_DGRAM, 0);
        if(sock<0)
                cerr<<"socket error"<<endl;
                return 2;
        
        struct sockaddr_in dest;
        memset(&dest, 0, sizeof(dest));
        dest.sin_family=AF_INET;
        dest.sin_port=htons(atoi(argv[2]));//注意先htons
        dest.sin_addr.s_addr= inet_addr(argv[1]);//注意inet_addr为字符点分十进制转为二进制
        //注意client端不用绑定,而是由OS随机帮我们查找端口

        char buffer[1024];
        //模拟远程执行命令
        for(;;)
                cout<<"[RemoteTest@iZuf68hx5ixwnbhts04uheZ 4-6]# ";
                fflush(stdout);
                buffer[0]=0;
                ssize_t size=read(0, buffer, sizeof(buffer)-1);//从标准输出读数据
                if(size>0)
                        buffer[size-1]=0;
                        sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&dest, sizeof(dest));

                        struct sockaddr_in peer;
                        socklen_t len=sizeof(peer);
                        ssize_t s=recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&peer, &len);
                        if(s>0)
                                buffer[s]=0;//相当于结尾'\\0'
                                cout<< "echo back from server# "<<endl;
                                cout<<buffer<<endl;
                        
                
        
        close(sock);
        return 0;

问: client 为何不需要明确bind?
:注意,是可以绑定,如果你自己bind了,成功了还好,如果你的client端口被别的程序占用,你的client就无法启动,客户端不是必须的是哪一个端口,只需要有一个端口就可以。我们一般不自己bind,而是由OS随机帮我们查找端口

运行结果


更改远程执行命令并显示到客户端

popen()

注意udp不能用dup2(),udp是数据包套接字,而dup2一般是流文件,不能使用

#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);


功能说明:
popen()函数通过创建一个管道, 调用fork()产生- 个子进程, 执行一个shell以运行命令来开启一个进程。可以通过这个管道执行标准输入输出操作。这个管道必须由pclose()函数关闭,必须由pclose()函数关闭,必须由pclose()函数关闭,而不是fclose()函数 (若使用fclose则会产生僵尸进程)。pclose()函数关闭标准I/O流,等待命令执行结束,然后返回shel的终止状态。如果shell不能被执行,则pclose()返回的终止状态与shell已执行exit一样


command:包含 shell 的以空字符结尾的字符串的指针命令行, 该命令使用 -c flag传递给 /bin/sh,进行解释,如果有此命令,则由 shell 执行
type:同read,open等函数一样,(以只读,写…)

更改代码

更改server端程序替换部分代码 为以下代码:

if(s>0)
      FILE *in = popen(message, "r");
        if(in == nullptr)
                continue;
        string echo_message;
        char line[128];
        while(fgets(line, sizeof(line), in))
                echo_message+=line;
        
        sendto(sock,echo_message.c_str(),echo_message.size(),0, (struct sockaddr*)&peer,len);


④TCP示例代码及其注意点

单客户端连接

分为五个文件分别为tcp_server.hpp, tcp_client.hpp, tcp_handler.hpp, server.cc, client.cc
(hpp文件负责封装ns_tcp_server, ns_tcp_client和ns_handler类)

tcp_server.hpp

  1. 私有成员uint16_t port (端口); int listen_sock(用于获取新链接);typedef void (*handler_t)(int); (函数指针类型,用于Loop回调)
  2. InitTCPserver()初始化过程:三步:
    1.listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    2.bind(listen_sock, (struct sockaddr*)&local, sizeof(local))
    3.listen(listen_sock, backlog)注意:监听为tcp特有 ,tcp协议是面向连接的,即如果要正式传递数据之前,需要先建立链接
  3. 设计一个Loop(handler_t handler)回调机制函数,将handler进行单独封装(解耦),用回调函数调用
    1.循环sock = accept(listen_sock, (struct sockaddr*)&peer, &len)获取链接
    2.回调函数handler处理链接
    3.暂时关闭链接close(sock)

代码如下:

#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "tcp_handler.hpp" 
using namespace std; 
namespace ns_tcp_server
        typedef void (*handler_t)(int);//函数指针类型
        const int backlog=5;
        class TCPserver
                private:
                        uint16_t port;
                        int listen_sock;//获取新链接
                public:
                        TCPserver(int _port):
                                port(_port),
                                listen_sock(-1)
                        
                        void InitTCPserver()
                        
                                listen_sock = socket(AF_INET, SOCK_STREAM, 0);//1. 创建一个sock(fd文件0,1,2,3...)
                                if(listen_sock < 0)
                                        cout<<"sock error"<<endl;
                                        exit(2);
                                

                                struct sockaddr_in local;
                                bzero(&local, sizeof(local));//初始化结构体
                                local.sin_family = AF_INET;
                                local.sin_port = htons(port);
                                local.sin_addr.s_addr = INADDR_ANY;//本机上所有任意IP
                                if(bind(listen_

以上是关于Linux----网络编程socket的主要内容,如果未能解决你的问题,请参考以下文章

Socket详解-Linux Socket编程(不限Linux)

C/C++ Linux Socket网络编程

Linux网络编程(Socket)

Linux Socket编程

Linux Socket 网络编程

Linux网络编程 --- socket编程