Linux Socket 网络编程重要函数用法及注意事项

Linux Socket 网络编程重要函数用法及注意事项 {ignore}

Socket TCP 系统调用概述

TCP三次握手、四次挥手和连接状态切换图:
https://www.cnblogs.com/Victor-Tian/p/7842501.html

服务端: socket() -> bind() -> listen() -> accept() -> recv() -> send() -> close()
客户端: socket() -----------> connect() ------------> send() -> recv() -> close()

服务端函数作用

  1. socket()创建socket,生成文件描述符。
    注意:在服务端创建完成socket之后,可以设置socket的SO_REUSEADDR选项,因为Linux中,如果主机上有任何可匹配到本地端口的TCP连接,则本地端口不能被重复使用,即bind()一个已经使用的socket会失败。 该选项可解放该限制,大多数TCP服务器都应该开启这个选项。具体可参考实例。

  2. bind()将刚创建的socket绑定到本地地址上。
    注意:如果绑定的本地端口已经被使用,则会失败。通过设置socket的SO_REUSEADDR选项可避免该问题。
    造成绑定失败的原因可能如下。

  • 之前服务器的连接经过close()、或者直接崩溃,使得TCP结点处于TIME_WAIT状态,直到2*MSL超时后,才能重新绑定。
  1. listen()将socket置为侦听状态。
    注意:此时,listen状态下的socket已经可以接收来自客户端的connect了。

  2. accept()等待客户端连接connect。
    注意:默认设置下accept()会阻塞进程,直到有客户端connect后才继续执行后续程序。

  3. recv()接收客户端发送的数据。
    注意:默认设置下,阻塞进程,直到接收到数据。
    也可用read()函数来接收数据。

  4. send()发送数据到客户端。
    也可用write()函数来发送数据。

  5. close()关闭连接。
    注意:关闭连接够,连接经过“四次挥手”最终进入TIME_WAIT状态,正常的TIME_WAIT状态要等待2*MSL时间后,才会真正关闭连接。在socket未设置SO_REUSEADDR时,当有TCP结点处于TIME_WAIT状态时,是无法通过该结点创建新的连接。
    TIME_WAIT作用如下。

  • 实现可靠的连接终止,在必要时重传最后一帧ACK包。
  • 让老的重复报文段在网络中过期失效。如果无TIME_WAIT,close()后立刻创建新的相同socket结点,那么,网络中老的重复报文段会在新连接中被当做合法报文处理。

客户端函数作用

  1. socket()创建socket,生成文件描述符。

  2. connect()连接到指定地址、端口的 处于listen或者connect状态下的服务端结点。

  3. send()发送数据到服务端。
    也可用write()函数来发送数据。

  4. recv()接收服务端的数据。
    也可用read()函数来接收数据。

函数详细说明

下列函数的详细参数说明,可参考:
http://net.pku.edu.cn/~yhf/linux_c/

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

#include<sys/types.h>
#include<sys/socket.h>

/*
 *  @Description: 创建socket,生成文件描述符
 *  @Para       : int domain 指定地址类型,常用如下几种
 *                      AF_UNIX     进程间socket通信协议
 *                      AF_INET     Ipv4网络协议
 *                      AF_INET6    Ipv6网络协议
 *                int type   指定socket类型,常用如下几种
 *                      SOCK_STREAM TCP类socket
 *                      SOCK_DGRAM  UDP类socket
 *                int protocal 用来指定socket所使用的传输协议编号,通常此参考不用管它,设为0即可。
 * 
 *  @return     : 成功返回socket的文件描述符,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int socket(int domain,int type,int protocol);

int bind (int sockfd,struct sockaddr * my_addr,int addrlen)

注意:如果绑定的本地端口已经被使用,则会失败。通过设置socket的SO_REUSEADDR选项可避免该问题。

#include<sys/types.h>
#include<sys/socket.h>

/*
 *  @Description: 将socket创建的socket结点,绑定到本地的IP、端口上。
 *  @Para       : int sockfd 服务端的文件描述符,由服务端创建socket时生成。
 *                const struct sockaddr * my_addr    存储本地IP地址、端口号等信息的结构体指针,为了兼容IPv4 IPv6,本处使用了兼容模式的结构体。具体用法参考实例代码。
 *                int addrlen 存储本地地址信息的my_addr结构体缓存区长度。(由于IPv4 IPv6等地址协议类型不同,会造成结构体存储的内容不同)
 * 
 *  @return     : 成功返回socket的文件描述符,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int bind (int sockfd,const struct sockaddr * my_addr, int addrlen);

/*为了兼容IPv4 IPv6的结构体*/
struct sockaddr
{
    unsigned short int sa_family;   //AF_INET(IPv4) 或 AF_INET6(IPv6)
    char sa_data[14];
};

/*IPv4 协议的地址参数结构体*/
struct socketaddr_in
{
    unsigned short int sin_family;  //AF_INET 只能能是IPv4类型
    uint16_t sin_port;
    struct in_addr sin_addr;
    unsigned char sin_zero[8];
};
struct in_addr
{
    uint32_t s_addr;
};

/*IPv6 协议的地址参数结构体*/
struct sockaddr_in6
{
    uint16_t sin6_family;       /* Address family AF_INET6 */  //只能是IPv6类型
    uint16_t sin6_port;         /* Transport layer port # */
    uint32_t sin6_flowinfo;     /* IPv6 flow information */
    struct in6_addr sin6_addr;  /* IPv6 address */
    uint32_t sin6_scope_id;     /* IPv6 scope-id */
};
struct in6_addr
{
    uint8_t s6_addr[16];        /* IPv6 address */
};

int listen (int sockfd, int backlog)

注意:此时,listen状态下的socket已经可以接收来自客户端的connect了,在此之前的客户端connect均会被忽略

#include<sys/types.h>
#include<sys/socket.h>
/*
 *  @Description: 将服务端socket创建的结点置为listen状态,可以接收来自客户端的connect。
 *  @Para       : int sockfd       要设置为listen状态的服务端socket的文件描述符
 *                int backlog      缓冲客户端的connect请求的数量。由于listen状态下就已经可以接收客户端的connect,
 *                                 但必须要服务端进入accept()才能处理,此参数可设置最多缓存待处理的connect的个数。
 * 
 *  @return     : 成功返回0,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int listen(int sockfd, int backlog);

int accept (int s,struct sockaddr * addr,int * addrlen)

注意:该函数中 int * addrlen 既是一个输入参数,也是一个输出参数,在调用accept()之前,一定要初始化指针指向的内容值。详见函数参数注释。
另外,该函数会阻塞进程,直到接收到客户端连接connect。

#include<sys/types.h>
#include<sys/socket.h>

/*
 *  @Description: 用来接收参数sockfd的socket连线,参数sockfd的socket必需先经bind()、listen()函数处理过。
 *  @Para       : int sockfd                服务端的文件描述符,由服务端创建socket时生成。
 *                struct sockaddr * addr    接受到客户端的connect后,存储客户端IP地址、端口等信息的结构体指针
 *                int * addrlen             既是输入参数,也是输出参数,调用accept()之前,要将该指针指向的值设置为addr的缓冲区长度,
 *                                          accept()返回后,这个值存储了复制到addr缓存区中的数据字节数(即返回的客户端IP、端口信息长度)。
 * 
 *  @return     : 成功返回客户端的socket的文件描述符,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int accept(int sockfd,struct sockaddr * addr,int * addrlen);

int connect (int sockfd, const struct sockaddr * serv_addr, int addrlen)

注意:
如果connect()失败或者connect()成功之后又断开了连接,希望重新建立连接,可移植的方式是,先关闭失败或者断开连接的客户端socket,创建一个新的socket,在新socket上重新建立连接。

#include<sys/types.h>
#include<sys/socket.h>

/*
 *  @Description: 用于客户端连接服务端。
 *  @Para       : int sockfd                        客户端中由socket创建的结点的文件描述符
 *                const struct sockaddr * addr      要连接到的服务端IP地址、端口等信息的结构体指针
 *                int addrlen                       addr的结构体数据长度。
 * 
 *  @return     : 成功返回0,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int connect(int sockfd, const struct sockaddr * addr, int addrlen);

int recv (int sockfd, void *buf, int len, unsigned int flags)

默认接收时未收到数据会阻塞进程,将flags设置为MSG_DONTWAIT可调整为非阻塞执行,非阻塞情况下,若未接收到数据,则立即报错返回,错误代码在erron中。

#include<sys/types.h>
#include<sys/socket.h>

/*
 *  @Description: 用于接收远端的数据。
 *  @Para       : int sockfd            远端结点的文件描述符
 *                void * buf            接收到的数据缓存区
 *                int len               缓存区buf的长度
 *                unsigned int flags    接收配置选项,一般设置为0。详细说明参考本节开头的网址
 * 
 *  @return     : 成功返回接收到的字节数,接收到文件结束符EOF时返回0,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int recv (int sockfd, void *buf, int len, unsigned int flags)

int send (int sockfd, const void *buf, int len, unsigned int flags)

默认发送缓存区满的时候会阻塞进程,将flags设置为MSG_DONTWAIT可调整为非阻塞执行,非阻塞情况下,发送时若发送缓存以满,则立即报错返回,错误代码在erron中。

#include<sys/types.h>
#include<sys/socket.h>

/*
 *  @Description: 用于发送数据到远端。
 *  @Para       : int sockfd            远端结点的文件描述符
 *                const void * buf      要发送的数据缓存区
 *                int len               缓存区中要发送的数据长度
 *                unsigned int flags    发送配置选项,一般设置为0。详细说明参考本节开头的网址
 * 
 *  @return     : 成功返回发送的字节数,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int send (int sockfd, const void *buf, int len, unsigned int flags)

int close (int fd)

#include<unistd.h>
close为双端关闭,调用后,不能发送,也不能接收。若要实现单端关闭,将全双工变为单工,则可参考shutdown()函数功能。
close关闭后,TCP会经历四次挥手过程,进入TIME_WAIT状态,经过2*MSL时间后,才会真正关闭连接。
/*
 *  @Description: 用于发送数据到远端。
 *  @Para       : int fd            要关闭的socket结点文件描述符
 * 
 *  @return     : 若文件顺利关闭则返回0,发生错误时返回-1
**/
int close(int fd);

服务端多线程接收数据实例

头文件 socket_server.h 代码如下

#ifndef _SOCKET_SERVER_H_
#define _SOCKET_SERVER_H_

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <string>
#include <pthread.h>

using namespace std;

#define MAX_WAIT_CONNECTION  20 //进入侦听状态后,服务端所能缓存的最多connect请求数量;
#define REVBUFFLEN      200

typedef struct serverparam
{
    int client_fd;                      //客户端文件表示符
    unsigned int order;                 //该节点在vector中的序号
    pthread_t client_pthread_id;        //客户端接收任务的任务id
    socklen_t client_struct_len;        //客户端结构长度
    struct sockaddr_in client_addr;     //客户端参数
    char revbuf[REVBUFFLEN];            //接受缓存区
}SRVERPARAM;


void * server_accept(void *);
void * server_rev(void * arg);
void Ctrl_C_fun(int sig);
void DeleteClient(SRVERPARAM * ClientNow);

#endif

服务端具体实现文件 socket_server.cpp

#include <vector>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include "socket_server.h"
#include <iostream>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>

int server_fd;      //服务端文件标识符
struct sockaddr_in server_addr;     //服务端地址结构
int ClientSum = 0;  //系统中与服务端连接的客户端总数

int main(void)
{
    pthread_t server_pthread_id;
    server_addr.sin_family = AF_INET;    //IPv4协议
    server_addr.sin_port = htons(8000);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY) ; //inet_addr("192.168.127.131")
    server_fd = socket( server_addr.sin_family, SOCK_STREAM, IPPROTO_TCP);
    if(server_fd == -1)
    {
        printf("socket create failed!\r\n");
        return 0;
    }
    int optval = 1;
    if(setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1)
        printf("socket SO_REUSEADDR option set failed!\r\n");
    
    if(bind(server_fd, (struct sockaddr *)&server_addr, (socklen_t)sizeof(struct sockaddr_in)) == -1)
    {
        printf("bind failed!\r\n");
        return 0;
    }

    if(listen(server_fd, MAX_WAIT_CONNECTION) == -1)
    {
        printf("listen failed!\r\n");
        return 0;
    }

    pthread_attr_t server_accept_attr;
    
    if(pthread_create(&server_pthread_id, NULL, server_accept, NULL) == -1)
    {
        printf("server_accept pthread create failed\r\n");
        return 0;
    }
    
    while (1)
    {
        /* code */
    }
    
    close(server_fd);
    return 0;
}

/*
 *  @Description: pthread of server accept client connect.
 *  @Para       : NULL
 *  @return     : NULL
 *  @Author     : Huge
 *  @Data       : 2020.02.10 22:50
**/
void * server_accept(void * arg)
{
    printf("Waiting for client connect!\r\n");
    pthread_detach(pthread_self()); //线程退出后自行删除线程资源
    while (1)
    {
        SRVERPARAM * ClientNow = (SRVERPARAM *)malloc(sizeof(SRVERPARAM));  //为客户端申请临时空间
        if(ClientNow == NULL)
        {
            cout << "server_accept malloc SRVERPARAM failed!\r\n" << endl;
            continue;
        }
        
        ClientNow -> client_struct_len = sizeof(struct sockaddr);  //使用accept之前一定要初始化
        ClientNow -> client_fd = accept(server_fd, (struct sockaddr *)&(ClientNow -> client_addr), &(ClientNow -> client_struct_len));
        if(ClientNow -> client_fd == -1)
            continue;

        struct  hostent * client_host = NULL;
        client_host = gethostbyaddr(&ClientNow -> client_addr.sin_addr, 4, AF_INET);
        if(client_host == NULL)
            printf("Get Client host failed!\r\n");
        else
            printf("Client host name %s\r\n", client_host -> h_name);
        
        if(pthread_create(&ClientNow -> client_pthread_id, NULL, server_rev, (void *)ClientNow) == -1)
        {
            free(ClientNow);
            printf("server_rev pthread create failed\r\n");
        }
        else
        {
            ClientSum++;
            printf("Client IP %s port %d.\r\n", inet_ntoa(ClientNow -> client_addr.sin_addr) , ClientNow -> client_addr.sin_port);
            printf("Connection sum is %d\r\n", ClientSum);
        }
    }
    return NULL;
}

/*
 *  @Description: pthread of server receive data from client.
 *  @Para       : the struct SRVERPARAM address of client.
 *  @return     : NULL
 *  @Author     : Huge
 *  @Data       : 2020.02.10 22:50
**/
void * server_rev(void * arg)
{
    pthread_detach(pthread_self()); //线程退出后自行删除线程资源
    SRVERPARAM * ClientNow = (SRVERPARAM *)arg;
    while (1)
    {
        struct timeval timeout={5,0};   //接收的timeout延时为5s
        int ret = setsockopt(ClientNow -> client_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout,sizeof(timeout));
        if(ret != 0)
        {
            DeleteClient(ClientNow);
            break;
        }
        
        int revlen = recv(ClientNow -> client_fd, ClientNow -> revbuf, REVBUFFLEN - 1, 0);
        if(revlen == -1) //超时
        {
            switch (errno)
            {
                case EAGAIN:
                {
                    printf("Waiting IP %s PORT %d data timeout!\r\n", 
                           inet_ntoa(ClientNow -> client_addr.sin_addr),
                           ClientNow -> client_addr.sin_port);
                    break;
                }
                default:
                    printf("recv IP %s PORT %d data failed!\r\n", 
                           inet_ntoa(ClientNow -> client_addr.sin_addr),
                           ClientNow -> client_addr.sin_port);
                    break;
            }
            DeleteClient(ClientNow);
            break;  //退出线程,删除该线程的接收任务
        }
        else    //正常接收
        {
            ClientNow -> revbuf[revlen] = '\0';
            printf("IP %s PORT %d : %s\r\n", 
                   inet_ntoa(ClientNow -> client_addr.sin_addr), 
                   ClientNow -> client_addr.sin_port, 
                   ClientNow -> revbuf);
        }
    }
    return NULL;
}

/*
 *  @Description: Delete useless client connection, and free the malloc RAM.
 *  @Para       : the struct SRVERPARAM address of client.
 *  @return     : void
 *  @Author     : Huge
 *  @Data       : 2020.02.10 22:50
**/
void DeleteClient(SRVERPARAM * ClientNow)
{
    close(ClientNow -> client_fd);
    ClientSum--;
    printf("Client IP %s PORT %d closed! connection sum %d\r\n", 
            inet_ntoa(ClientNow -> client_addr.sin_addr), 
            ClientNow -> client_addr.sin_port, 
            ClientSum);
    free(ClientNow);
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值