前言

        socket(套接字)是linux下进程间通信的一种方式,通常使用C-S(客户端-服务端)的方式通信,它可以是同一主机下的不同进程间通信或者不同主机的进程通信。

        socket是夹在应用层和TCP/UDP协议层间的软件抽象,向应用层开发人员提供API接口,向下隐藏协议层的具体细节,大大方便了我们开发人员。很多平台都实现了BSD scoket标准scoket接口,增强了可移植性。

        在进行socket网络编程之前,有必要对计算机网络有个大概的了解,这里推荐一篇博文,链接如下:TCP/IP协议族之TCP、UDP协议详解

1. socket概述

1.1 表示方法

Socket=(IP地址:端口号),套接字的表示方法是点分十进制的lP地址后面加上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点所确定。

1.2 socket主要类型

 流套接字(SOCK_STREAM):用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。

 数据报套接字(SOCK_DGRAM):提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。

 原始套接字(SOCK_RAW):原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接。

2. socket-API接口

2.1 socket函数

socket函数用于初始化创建一个通信端点,调用成功返回一个socket描述符,类似于open函数的文件描述符;

可以使用close函数来关闭socket,释放占用的资源。

头文件:

#include <sys/types.h>

#include <sys/socket.h>

函数原型:

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

参数:

domain:指示通信协议族;

        AF_UNIX或 AF_LOCAL:Local communication本地通信;

        AF_INET:IPv4 Internet protocols,就是我们通常用的ipv4地址;

        AF_INET6:IPv6 Internet protocols,ipv6地址;

        AF_NETLINK:Kernel user interface device;

        其它;

type:指定套接字的类型;

        SOCK_STREAM:提供有序的、可靠的、双向的、基于连接的字节流,默认TCP协议;

        SOCK_DGRAM:固定长度的、无连接的、不可靠的报文传递,默认UDP协议;

        SOCK_RAW:允许应用程序访问网络层的原始数据包,原始套接字;

protocol:通常设置为0,表示为给定的通信域和套接字类型选择默认协议。

返回值:

成功:返回非负数socket描述符;

失败:返回 -1。

2.2  bind函数

bind函数通常用于服务端,将服务端的socket文件与网络中的进程地址(IP地址+端口号)绑定;

指向sockaddr_in的结构体指针也可以指向sockaddr的结构体,所以经常使用sockaddr_in替代。

头文件:

#include <sys/types.h>

#include <sys/socket.h>

函数原型:

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

参数:

sockfd:socket描述符;

addr:地址(ip地址+端口号);

addrlen:第二个参数addr长度;

返回值:

成功:返回 0;

失败:返回 -1。

struct sockaddr { 
    sa_family_t sa_family; 
    char sa_data[14]; 
}


struct sockaddr_in {
    sa_family_t sin_family;  //协议族
    in_port_t sin_port;      //端口号
    struct in_addr sin_addr; // IP 地址
    unsigned char sin_zero[8]; 
};

2.3 listen函数

listen函数调用进入监听状态,通常用于服务端,服务端调用listen函数进入监听状态,等待客户端的连接请求。

参数backlog表明连接等待队列的上限,如果该服务端已经连接上一个客户端,之后其它客户端的连接请求将进入连接等待队列,同时该队列会有个上限值,不可能无限大。

头文件:

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

函数原型:

int listen(int sockfd, int backlog)

参数:

backlog:连接等待队列上限值;

返回值:

成功:返回  0;

失败:返回 -1。

2.4 accept函数

accept()函数用于获取客户端的连接请求并建立连接,通常用于服务端;它是一个阻塞函数,如果没有客户端的连接请求,则会阻塞等待;accept()函数返回的套接字连接到调用connect()的客户端,服务端通过该套接字与客户端进行数据交互。

头文件:

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

函数原型:

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

参数:

sockfd:socket()函数返回的描述符;

addr:struct sockaddr指针变量,是一个传出参数,用于存放客户端的连接信息,可设为NULL,表示不关心客户端信息;

addrlen:表明传出参数addr的字节长度;可设为NULL;

返回值:

成功:返回一个新的socket描述符;

失败:返回 -1 。

2.5 connect函数

通常用于客户端,调用connect函数请求与服务端连接;常说的三次握手就是由connect触发的。

函数原型:

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

参数:

sockfd:调用socket生成的描述符;

addr:指定服务端的IP地址以及端口号等信息;

addrlen:表明addr信息长度;

返回值:

成功:返回 0;

失败:返回 -1。

2.6 发送函数

2.6.1 send函数

send函数通常用于TCP数据包的发送。

send()成功返回只能表示数据包已经发送出去了,并不能代表接收端已经接收到数据了。

函数原型:

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

参数:

sockfd:socket函数返回值;

buf:发送的数据缓冲区;

len:发送的数据大小;

flags:标志位,通常设为0;

        MSG_DONTWAIT:允许非阻塞操作;

        MSG_MORE:延迟发送数据包允许写更多数据;

        其它......

返回值:

失败:返回 -1;

2.6.2 sendto函数

常用于UDP数据发送,因为UDP是无连接的,因此需要指定目的地址。

函数原型:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen)

参数:

dest_addr:数据发送的目的地址;

addrlen:目的地址长度;

2.7 接收函数

2.7.1 recv函数

用于TCP数据的接收;阻塞模式下,直到有数据才返回。

函数原型:

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

参数:

buf:数据接收缓冲区;

flags:一般设为0;

        MSG_DONTWAIT:启动非阻塞操作;

        MSG_PEEK:返回数据包内容而不真正取走数据包;

        其它......

返回值:

成功:返回接收的数据长度;

失败:返回 -1。

2.7.2 recvfrom函数

一般用于UDP数据的接收。

函数原型:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen)

参数:

src_addr:指向数据源端地址。

2.8 IP地址格式转化

2.8.1 inet_ntop函数

用于二进制形式字符串转为十进制形式字符串。

头文件:

#include <arpa/inet.h>

函数原型:

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size)

参数:

af:为AF_INET(ipv4)或AF_INET6(ipv6);

src:需要转化的源字符串;一般为struct in_addr结构体的对象;

dst:转换生成的字符串存放的缓冲区;

size:缓冲区的大小;

返回值:

成功:返回指向dst的指针;

失败:返回NULL。

代码示例:

#include <stdio.h>
#include <arpa/inet.h>

int main()
{
    struct in_addr src_addr;
    char dst_addr[32];
    src_addr.s_addr = 0x6401a8c0;
    inet_ntop(AF_INET,&src_addr,dst_addr,sizeof(dst_addr));
    printf("dst addr:%s\n",dst_addr);
    return 0;
}

2.8.2 inet_pton函数

用于十进制形式字符串转为二进制形式字符串,返回值为大端存储。

头文件:

#include <arpa/inet.h>

函数原型:

int inet_pton(int af, const char *src, void *dst)

返回值:

成功:返回1;

格式无效:返回0;

失败:返回-1。

struct in_addr {
    __be32  s_addr;
};

示例代码:

#include <stdio.h>
#include <arpa/inet.h>

#define IP_ADDR "192.168.1.100"
int main()
{
    struct in_addr dst_addr;
    inet_pton(AF_INET,IP_ADDR,&dst_addr);
    printf("dst addr:%x\n",dst_addr.s_addr);
    return 0;
}

2.9 存储顺序

host:主机字节序(小端存储);

network:网络字节序(大端存储)。

2.9.1 主机转网络存储顺序

头文件:

#include <arpa/inet.h>

函数原型:

uint32_t htonl(uint32_t hostlong);        //32位IP地址

uint16_t htons(uint16_t hostshort);      //16位端口号

INADDR_ANY指定地址为0.0.0.0地址,表示监听所有的IP地址

2.9.2 网络转主机存储顺序

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);

3.  socket工作流程

3.1  TCP工作流程

3.2 UDP工作流程

4. TCP通信测试代码

4.1 服务端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define PORT 6666

int main()
{
    int socket_fd,fd;
    int ret;
    char send_buff[64] = "Data was send by server.";    //连接上发给客户端的数据
    char recv_buff[64];
    char client_ip[32];
    struct sockaddr_in client_msg;
    int msg_len = sizeof(client_msg);
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    socket_fd = socket(AF_INET,SOCK_STREAM,0);
    if(socket_fd < 0){
        perror("socket");
        exit(0);
    }
    ret = bind(socket_fd,(struct sockaddr *)&server_addr,sizeof(server_addr));
    if(ret == -1){
        perror("bind");
        close(socket_fd);
        exit(0);
    }

    ret = listen(socket_fd,32);
    if(ret == -1){
        perror("listen");
        close(socket_fd);
        exit(0);
    }
    printf("listen...\n");
    fd = accept(socket_fd,(struct sockaddr *)&client_msg,&msg_len);
    inet_ntop(AF_INET,&client_msg.sin_addr.s_addr,client_ip,sizeof(client_ip));
    printf("client ip addr:%s,port:%d\n",client_ip,client_msg.sin_port);
    send(fd,send_buff,sizeof(send_buff),0);
    while(1)
    {
        memset(recv_buff,0,sizeof(recv_buff));
        recv(fd,recv_buff,sizeof(recv_buff),0);    //阻塞接收客户端的数据
        printf("recv buff :%s\n",recv_buff);
        if(strncmp("quit",recv_buff,4) == 0){
            printf("socket closer.\n");
            close(fd);
            close(socket_fd);
            exit(0);
        }
    }
    return 0;
}

4.2 客户端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc,char *argv[])
{
    int socket_fd;
    int ret;
    char send_buff[] = "From client datas.";
    char recv_buff[64];
    struct sockaddr_in client_msg;

    if(argc < 3){
        exit(0);
    }
    printf("ip:%s,port:%s\n",argv[1],argv[2]);
    client_msg.sin_family = AF_INET;
    inet_pton(AF_INET, argv[1], &client_msg.sin_addr);    //传入的ip地址
    client_msg.sin_port = htons(atoi(argv[2]));    //传入的端口号
    htonl(client_msg.sin_addr.s_addr);

    socket_fd = socket(AF_INET,SOCK_STREAM,0);
    if(socket_fd == -1){
        perror("socket");
        exit(0);
    }
    printf("socket_fd :%d\n",socket_fd);
    ret = connect(socket_fd,(struct sockaddr *)&client_msg,sizeof(client_msg));  //发起连接
    if(ret == -1){
        perror("connect");
        close(socket_fd);
        exit(0);
    }
    recv(socket_fd,recv_buff,sizeof(recv_buff),0);
    printf("recv buff : %s\n",recv_buff);

    while(1)
    {
        memset(send_buff,0,sizeof(send_buff));
        printf("client data input: ");
        fgets(send_buff,sizeof(send_buff),stdin);    //从键盘输入数据信息
        send(socket_fd,send_buff,sizeof(send_buff),0);
        if(strncmp("quit",send_buff,4) == 0){
            close(socket_fd);
            exit(0);
        }
    }
    return 0;
}

4.3 测试结果

在Ubuntu下打开两个shell终端,分别运行服务端程序和客户端程序。

客户端传入服务端ip地址和端口号的参数,然后向服务端发送数据信息测试,结果如下图。

Logo

更多推荐