网络编程-socket通信(一)

socket通信(一)

底层封装了TCP/IP协议簇,开发者只要会用socket即可调用底层协议功能。

socket分为以下两种通信机制:

  • stream(流):TCP,基于连接,有序可靠
  • datagram(数据包):UDP,不建立连接,不可靠,效率比较高

现在用UDP的场景越来越少了,目前实时音视频聊天会用UDP数据包传输。

下图是socket通信流程:

程序概要

  1. socket文件描述符

    sockfd = socket(AF_INET,SOCK_STREAM,0),socket()函数的返回值其本质是一个文件描述符,是一个整数。

  2. 绑定ip地址和端口:

    // 第2步:把服务端用于通信的地址和端口绑定到socket上。
    struct sockaddr_in servaddr;    // 服务端地址信息的数据结构。
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;  // 协议族,在socket编程中只能是AF_INET。
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);          // 任意ip地址。
    //servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); // 指定ip地址。
    
    // htons()--"Host to Network Short"使用网络字节顺序,按从高到低的顺序存储(大端)
    // argv[1]表示后面接的参数
    // atoi 字符转为整数
    servaddr.sin_port = htons(atoi(argv[1]));  // 指定通信端口。
    if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 )
    { perror("bind"); close(listenfd); return -1; }
    

    client类似server,进行一样的操作,只不过将bind换为connect,连接至目标地址。

  3. server要打开监听

    if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }
    
  4. server接收client的连接

    int  clientfd;                  // 客户端的socket。
    int  socklen=sizeof(struct sockaddr_in); // struct sockaddr_in的大小
    struct sockaddr_in clientaddr;  // 客户端的地址信息。
    // accept后两个参数也可以给0,就看不到client地址
    clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen);
    printf("客户端(%s)已连接。\n",inet_ntoa(clientaddr.sin_addr));
    

    server listen之后,client即可连接,只不过发送的信息都在buffer中,server也不会响应。

    accept会返回一个socket,之后server向给client发送消息就是发给这个socket。

  5. client发送消息,server接收消息

    // 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。
    for (int ii=0;ii<3;ii++)
    {
        int iret;
        memset(buffer,0,sizeof(buffer));
        sprintf(buffer,"这是第%d个超级女生,编号%03d。",ii+1,ii+1);
        if ( (iret=send(sockfd,buffer,strlen(buffer),0))<=0) // 向服务端发送请求报文。
        { perror("send"); break; }
        printf("发送:%s\n",buffer);
    
        memset(buffer,0,sizeof(buffer));
        if ( (iret=recv(sockfd,buffer,sizeof(buffer),0))<=0) // 接收服务端的回应报文。
        {
            printf("iret=%d\n",iret); break;
        }
        printf("接收:%s\n",buffer);
    }
    

    ssize_t send(int sockfd, const void *buf, size_t len, int flags);

    sockfd为已建立好连接的socket(可以是accept返回的,也可以是connect建立的)。

    buf为需要发送的数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,内存中有什么就发送什么。

    len需要发送的数据的长度,为buf中有效数据的长度。

    flags填0, 其他数值意义不大。

    函数返回已发送的字符数。出错时返回-1,错误信息errno被标记。

    注意,就算是网络断开,或socket已被对端关闭,send函数不会立即报错,要过几秒才会报错。

    如果send函数返回的错误(<=0),表示通信链路已不可用。


    // 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
    char buffer[1024];
    while (1)
    {
        int iret;
        memset(buffer,0,sizeof(buffer));
        if ( (iret=recv(clientfd,buffer,sizeof(buffer),0))<=0) // 接收客户端的请求报文。
        {
            printf("iret=%d\n",iret); break;  
        }
        printf("接收:%s\n",buffer);
    
        strcpy(buffer,"ok");
        if ( (iret=send(clientfd,buffer,strlen(buffer),0))<=0) // 向客户端发送响应结果。
        { perror("send"); break; }
        printf("发送:%s\n",buffer);
    }
    

    ssize_t recv(int sockfd, void *buf, size_t len, int flags);

    sockfd为已建立好连接的socket。

    buf为用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,只要是一块内存就行了。

    len需要接收数据的长度,不能超过buf的大小,否则内存溢出。

    flags填0, 其他数值意义不大。

    函数返回已接收的字符数。出错时返回-1,失败时不会设置errno的值。

    如果socket的对端没有发送数据,recv函数就会等待,如果对端发送了数据,函数返回接收到的字符数。出错时返回-1。如果socket被对端关闭,返回值为0。

    如果recv函数返回的错误(<=0),表示通信通道已不可用。

  6. 关闭socket

    // 第6步:关闭socket,释放资源。
    close(listenfd); close(clientfd);
    

socket()函数详解

socket函数用于创建一个新的socket,也就是向系统申请一个socket资源。socket函数用户客户端和服务端。

int socket(int domain, int type, int protocol);

domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用。

protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

第一个参数只能填AF_INET,第二个参数只能填SOCK_STREAM(TCP),第三个参数只能填0。

除非系统资料耗尽,socket函数一般不会返回失败。

返回值:成功则返回一个socket,失败返回-1,错误原因存于errno 中。一般会返回3

可以通过man socket查看编程手册

可打开的socket连接数和linux允许打开的文件数相等,涉及到网络并发压力测试时要修改

sockaddr_in结构体

struct sockaddr_in
  {
    /*
    #define	__SOCKADDR_COMMON(sa_prefix) \
  		sa_family_t sa_prefix##family
    */
    __SOCKADDR_COMMON (sin_); //等同于 sa_family_t sin_family 表示地址类型
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */
	
    // 为了保持和sockaddr(旧版结构体)一样的长度
    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr)
			   - __SOCKADDR_COMMON_SIZE
			   - sizeof (in_port_t)
			   - sizeof (struct in_addr)];
  };
struct in_addr
  {
    // typedef uint32_t in_addr_t;
    in_addr_t s_addr;
  };

所以声明结构体时,只需要声明三个即可:

  • sockaddr_in.sin_family = AF_INET
  • sockaddr_in.sin_port
  • sockaddr_in.sin_addr.s_addr

最后调用bind函数绑定结构体到socket上即可:

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数要求旧的结构体sockaddr ,所以要强转成sockaddr类型

bind函数问题解决

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

  • 监听的端口号有限制,实际就是sockaddr内的端口限制,因为他是用的short int,所以最多表示2的16次方-1:65535。

  • 释放的端口要等待两分钟才可以再使用,TIME_WAIT。解决方法:

    // socket创建成功后,额外设置一步SO_REUSEADDR
    int opt = 1;
    unsigned int len = sizeof(opt);
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, len)
    

accept函数

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

从已经准备好的连接队列里面获取一个请求,如果队列为空,则阻塞等待,获取了一个连接,队列就会少一个

会返回一个socket文件描述符。

服务端函数调用的流程是:socket->bind->listen->accept->recv/send->close

客户端函数调用的流程是:socket->connect->send/recv->close

其中send/recv可以进行多次交互。

listen负责建立连接,accept负责处理连接。

结合TCP理解

内核为处于listen状态的socket维护两个队列:

  1. 不完全连接请求队列(SYN_RECV队列)
  2. 等待accept建立socket的队列(ESTABLISHED队列)

listen函数:

int listen(int sockfd, int backlog);

第二个参数backlog指等待accept的完全建立的socket的队列长度-2(完成三次握手的队列长度)

而不完全连接请求队列长度使用/proc/sys/net/ipv4/tcp_max_syn_backlog设置,默认值为128(ubuntu20.04为512)

程序中设置backlog为3,实际可以建立的ESTABLISHED连接为5个;

把backlog换成1,服务器ESTABLISHED连接为3个。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值