套接字通信

背景

基于C语言,对linux系统下套接字通信相关的知识点进行梳理,比如重点概念的理解,重点操作函数的解析等,最后附上相关示例代码。

概念

套接字分类

  • 流式套接字(SOCK_STREAM)
  • 数据报套接字(SOCK_DGRAM)
  • 原始套接字

流式套接字

使用TCP(传输控制协议)进行数据传输,可以保证数据传输的准确性。

数据报套接字

使用UDP(使用者数据报协议)进行数据传输,不能保证接收的数据的准确性。

相关数据结构

struct sockaddr

#include <sys/socket.h>
struct
sockaddr {   unsigned short sa_family;//地址协议族 char sa_data[14];//地址(ip + port) };

  struct sockaddr 是通用的套接字地址,长度为16字节。

struct sockaddr_in

#include <netinet/in.h>
/* Internet address. */
struct in_addr
{
  uint32_t s_addr;
};
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
  unsigned short sa_family;
  uint16_t sin_port; /* Port number. */必须是网络字节序
  struct in_addr sin_addr; /* Internet address. */必须是网络字节序
  unsigned char sin_zero[8];/* Pad to size of `struct sockaddr'. */
};

   internet环境下套接字的地址形式,长度也是16字节;

  因为bind()函数的套接字地址类型是通用类型,所以现在通行的做法是,使用struct sockaddr_in绑定ip和端口,然后强转成struct sockaddr类型  

本机转换

由于struct sockaddr_in的Ip和端口是数据需要发送到网络端,所以类型必须是网络字节序;

端口的转换需要用到下面的htons

#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort); uint32_t htonl(uint32_t hostlong); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);

其实,ip的转换也可以用htonl,但入参是uint32_t,需要先将一个字符串类型的IP换算成数值类型再传参;
考虑到htonl的使用有些繁琐,一般我们使用下面的函数来进行地址的转换:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);

  inet_addr()和inet_aton()都可以用于获取一个网络字节序的地址;
  inet_ntoa是逆操作;

  

#define INADDR_ANY ((in_addr_t) 0x00000000)

   INADDR_ANY是一个宏定义,数值是网络字节序,等价于inet_addr("0.0.0.0"),功能是代码所有本机IP

socket()

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

  domain 网络通信协议族,一般写AF_INET
  type 通信类型,SOCK_STREAM|SOCK_DGRAM
  protocol 定义额外的一个通信协议。通常只需要一个协议,所以这里填0
  返回:成功返回一个可用套接字;失败返回-1,并重置errno

setsockopt

#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);

  主要用于设置端口复用

fcntl

#include <unistd.h>
#include <fcntl.h> 
int fcntl(int fd, int cmd); 
int fcntl(int fd, int cmd, long arg); 
int fcntl(int fd, int cmd, struct flock *lock);

  常用于设置套接字非阻塞读

bind()

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

  给套接字绑定到 “本机通信地址”返回:
  成功返回一个可用套接字;失败返回-1,并重置errno

connect()

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

  将套接字与远程服务器通信地址绑定
  返回:成功返回一个可用套接字;失败返回-1,并重置errno

listen()

int listen(int sockfd, int backlog); 

  sockfd一般是服务器的网络侦听套接字,backlog是连接队列的长度(等待接受连接请求)
  返回:成功返0;失败返回-1并重置errno

accept()

#include <sys/types.h>
#include <sys/socket.h>
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

  返回一个成功建立连接的新套接字
  返回:成功返0;失败返回-1并设置errno

send()

#include <sys/types.h>
#include <sys/socket.h>
int send(int s, const void *msg, size_t len, int flags);
int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
int sendmsg(int s, const struct msghdr *msg, int flags);

  s 套接字
  msg 待发送的数据
  len 数据长度
  flags 填0

close()

#include <unistd.h>
int close(int fd);

  完全关闭连接

 

#include <sys/socket.h>
int shutdown(int sockfd, int how);
    how:
    --SHUT_RD      关闭读端
    --SHUT_WR      关闭写端
    --SHUT_RDWR    关闭读写(同close())

  相比close,有更多的控制

示例代码

 文件描述符设置阻塞与非阻塞

参考:https://www.cnblogs.com/xuyh/p/3273082.html

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

/**********************使能非阻塞I/O********************
*int flags;
*if(flags = fcntl(fd, F_GETFL, 0) < 0)
*{
*    perror("fcntl");
*    return -1;
*}
*flags |= O_NONBLOCK;
*if(fcntl(fd, F_SETFL, flags) < 0)
*{
*    perror("fcntl");
*    return -1;
*}
*******************************************************/

/**********************关闭非阻塞I/O******************
flags &= ~O_NONBLOCK;
if(fcntl(fd, F_SETFL, flags) < 0)
{
    perror("fcntl");
    return -1;
}
*******************************************************/

int main()
{
    char buf[10] = {0};
    int ret;
    int flags;
    
    //使用非阻塞io
    if(flags = fcntl(STDIN_FILENO, F_GETFL, 0) < 0)
    {
        perror("fcntl");
        return -1;
    }
    flags |= O_NONBLOCK;
    if(fcntl(STDIN_FILENO, F_SETFL, flags) < 0)
    {
        perror("fcntl");
        return -1;
    }

    while(1)
    {
        sleep(2);
        ret = read(STDIN_FILENO, buf, 9);
        if(ret == 0)
        {
            perror("read--no");
        }
        else
        {
            printf("read = %d
", ret);
        }
        
        write(STDOUT_FILENO, buf, 10);
        memset(buf, 0, 10);
    }

    return 0;
}

Socket服务器(多进程)

#include <stdio.h>                                                                                                                    
#include <stdlib.h>
#include <string.h>//memset
#include <errno.h>
#include <unistd.h>//fork

#include <sys/types.h>//socket
#include <netinet/in.h>//sockaddr
#include <sys/socket.h>//sockaddr
#include <arpa/inet.h>//sockaddr
#include <sys/wait.h>//wait

#define PORT 8888
#define BACKLOG 128
#define MAXLENGTH 1024

int main()
{
    int listenfd;//侦听套接字
    int new_fd;//新连接
    int ret;//返回值检查
    struct sockaddr_in my_addr;//本机地址
    struct sockaddr_in addr;//远程地址
    socklen_t addrlen;
    char buffer[MAXLENGTH] = {0};

    listenfd = socket(AF_INET, SOCK_STREAM, 0); 
    if (listenfd == -1){
        perror("socket");
        return -1; 
    }   

    memset(&my_addr, 0, sizeof(struct sockaddr_in));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(PORT);
    my_addr.sin_addr.s_addr = inet_addr("192.168.6.131");

    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt, sizeof(opt));

    ret = bind(listenfd, (struct sockaddr*)&my_addr, sizeof(struct sockaddr));
    if (ret == -1){
        perror("bind");
        return -1; 
    }   

    ret = listen(listenfd, BACKLOG);
    if (ret == -1){
        perror("listen");
        return -1; 
    }   

    while (1) 
    {   
        addrlen = sizeof(struct sockaddr_in);
        new_fd = accept(listenfd, (struct sockaddr*)&addr, &addrlen);
        if (new_fd == -1){
            perror("accept");
            continue;
        } 

        printf("new connection: %s
",
                inet_ntoa(addr.sin_addr));

        pid_t child = -1;
        child = fork();
        if (child == -1){
            perror("fork");
            continue;
        }
        if (child == 0){
            /**
             *子进程,与远程客户端通信
             */
            char *cur_addr = inet_ntoa(addr.sin_addr);
            while (1)
            {

                memset(buffer, 0, MAXLENGTH);
                ret = recv(new_fd, buffer, MAXLENGTH, 0);
                if (ret == -1){
                    perror("recv");
                }
                else if (ret == 0){
                    printf("client %s close.
", cur_addr);
                    close(new_fd);
                    exit(0);
                }
                else
                {
                    printf("receive %dBytes data
", ret);

                    buffer[ret-1] = ' ';
                    strncat(buffer, "world", 6);
                    ret = send(new_fd, buffer, ret+6, 0);
                    if (ret == -1){
                        perror("send");
                    }
                }


            }//---while over
            exit(0);
        }
        else
        {
            /**
             * 主进程,尝试回收子进程,并继续接收新连接。
             */
            pid_t child = -1;
            while ((child = waitpid(-1, NULL, WNOHANG)) > 0)
            {
                printf("wait child[%d] success.
", child);
            }
        }
    }//---while over
    return 0;
}

客户端

#include <stdio.h>                                                                                                                    
#include <stdlib.h>
#include <string.h>//memset
#include <errno.h>
#include <unistd.h>//fork

#include <sys/types.h>//socket
#include <netinet/in.h>//sockaddr
#include <sys/socket.h>//sockaddr
#include <arpa/inet.h>//sockaddr
#include <sys/wait.h>//wait

#define PORT 8888
#define BACKLOG 128
#define MAXLENGTH 1024

int main()
{
    int sockfd;//连接套接字
    int ret;//返回值检查
    struct sockaddr_in remote_addr;//本机地址
    char buffer[MAXLENGTH] = {0};

    sockfd = socket(AF_INET, SOCK_STREAM, 0); 
    if (sockfd == -1){
        perror("socket");
        return -1; 
    }   

    memset(&remote_addr, 0, sizeof(struct sockaddr_in));
    remote_addr.sin_family = AF_INET;
    remote_addr.sin_port = htons(PORT);
    remote_addr.sin_addr.s_addr = inet_addr("192.168.6.131");

    ret = connect(sockfd, (struct sockaddr*)&remote_addr, sizeof(struct sockaddr));
    if (ret == -1){
        perror("connect");
        return -1; 
    }   

    strncpy(buffer, "hello", 6); 
    while (1) 
    {   
        char *stdin_ret;
        stdin_ret = fgets(buffer, MAXLENGTH, stdin);
        if (stdin_ret == NULL){
            perror("fgets");
            continue;
        }   
        ret = send(sockfd, buffer, strlen(stdin_ret), 0); 
        if (ret == -1){
            perror("send");
        }   

        memset(buffer, 0, MAXLENGTH);
        ret = recv(sockfd, buffer, MAXLENGTH, 0); 
        if (ret == -1){
            perror("recv");
        }   
        else if (ret == 0){ 
            printf("server have closed me.
");
            close(sockfd);
            break;
        }
        else
        {
            printf("%s
", buffer);
        }
    }//---while over
    return 0;
}

参考

fcntl函数详解:https://www.cnblogs.com/xuyh/p/3273082.html

多进程服务器:https://blog.csdn.net/coolwriter/article/details/80496826

socket编程:https://www.cnblogs.com/liushui-sky/p/5609535.html

原文地址:https://www.cnblogs.com/orejia/p/12128866.html