一、socket概述

socket计算机中的一种网络传输机制,对TCP和UDP的封装,自动帮我们在底层完成各种协议操作,接收到数据包后返回到上层应用。

socket分为客户端和服务端,它的工作模型为:

二、socket 网络地址

2.1 网络字节序

关于字节序的概念可以查看计算机中的字节序

一般来说,计算机是低字节序,网络传输是高字节序,两者之间并不统一。使用时需要通过以下函数进行转换:

#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);

记忆方式

h表示host,n表示network,l表示32位长整数,s表示16位短整数。

例如端口号是16位(s),主机(h)转网络(n)字节序的函数为htons

2.2 IP地址转换函数

socket地址

计算机中的IP地址是一个32位长整数,因为ip地址最多为255.255.255.255,每个点位最多占1个字节=8位,所以IP地址为32位整数。

我们用的地址是一个sockaddr类型,它包含了地址族,端口号和IP地址等信息。不过它是很早以前的地址结构了,为了适应需要,现在衍生出了sockaddr_in等地址类型如下图所示。

但是为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。类型的定义如下:

struct sockaddr {
    sa_family_t sa_family; /* address family, AF_xxx */
    char sa_data[14]; /* 14 bytes of protocol address */
};

struct sockaddr_in {
    __kernel_sa_family_t sin_family; /* Address family */
    __be16 sin_port; /* Port number */
    struct in_addr sin_addr; /* Internet address */
    /* Pad to size of `struct sockaddr''. */
    unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
    sizeof(unsigned short int) - sizeof(struct in_addr)];
};

/* Internet address. */
struct in_addr {
    __be32 s_addr;
};
struct sockaddr_in6 {
    unsigned short int sin6_family; /* AF_INET6 */
    __be16 sin6_port; /* Transport layer port # */
    __be32 sin6_flowinfo; /* IPv6 flow information */
    struct in6_addr sin6_addr; /* IPv6 address */
    __u32 sin6_scope_id; /* scope id (new in RFC2553) */
};
struct in6_addr {
    union {
        __u8 u6_addr8[16];
        __be16 u6_addr16[8];
        __be32 u6_addr32[4];
    } in6_u;
    #define s6_addr in6_u.u6_addr8
    #define s6_addr16 in6_u.u6_addr16
    #define s6_addr32 in6_u.u6_addr32
};
#define UNIX_PATH_MAX 108
struct sockaddr_un {
    __kernel_sa_family_t sun_family; /* AF_UNIX */
    char sun_path[UNIX_PATH_MAX]; /* pathname */
};

一般我们常用的是sockaddr_in,简单用法为:

sockaddr_in clnt_addr;
clnt_addr.sin_family = AF_INET;
clnt_addr.sin_port = htons(9999);
clnt_addr.s_addr.sin_addr = INADDR_ANY;

地址转换函数

早期IPv4地址转换函数:

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

在后期引入IPv6后的转换函数支持IPv4和IPv6:

#include <arpa/inet.h>
// 字符串类型地址转换成整形
int inet_pton(int af, const char *src, void *dst);
// 整形地址转换成字符串类型
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

其中af是地址族,一般填写AF_INET表示以太网。

三、socket操作函数

3.1 socket

socket函数用于创建一个socket对象,在linux环境中,socket也是一个文件,因此该函数实际返回的是一个文件描述符。

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

参数说明

domain:
    - AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址。
    - AF_INET6 与上面类似,不过是来用IPv6的地址。
    - AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用。
type:
    - SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。。
    - SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。。
    - SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取
    - SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)。
    - SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。
protocol:
    - 0 默认协议
返回值:
    成功返回一个新的文件描述符,失败返回-1,设置errno。

3.2 bind

bind用于绑定地址到socket。

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

参数说明

sockfd: socket文件描述符。

addr: 构造出IP地址加端口号。

addrlen: sizeof(addr)长度。

返回值: 成功返回0,失败返回-1, 设置errno。

绑定前要先设置好地址:

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8000);

3.3 listen

listen用于监听某个端口号:

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

参数说明

sockfd: socket文件描述符。

backlog: 排队建立3次握手队列和刚刚建立3次握手队列的链接数和。

返回值: listen()成功返回0,失败返回-1。

查看系统默认backlog

> cat /proc/sys/net/ipv4/tcp_max_syn_backlog
128

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。

3.4 accept

服务端接受一个socket连接,此时的连接已经三次握手完成

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

参数说明

**sockdf: socket文件描述符。

**addr: 传出参数,返回链接客户端地址信息,含IP地址和端口号。

**addrlen: 传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小。

**返回值: 成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno。

三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-resultargument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。

3.5 connect

客户端连接服务端的函数,此时开始三次握手。

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

参数说明:

sockdf: socket文件描述符。

addr: 传入参数,指定服务器端地址信息,含IP地址和端口号。

addrlen: 传入参数,传入sizeof(addr)大小。

返回值:成功返回0,失败返回-1,设置errno。

3.6 recv和recvfrom

#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);

recv用于TCP协议读取数据:

sockfd: 连接的socket文件描述符。

buf: 用来保存读取数据的变量。

len: 读取的数据大小

flags: 读取标志,一般设置为0。

返回值:成功返回读取到的字节数,失败返回-1,0表示已经断开连接。

recv用于udp协议,参数含义类似。

由于socket也是一个文件描述符,因此也可以使用read来读取socket中的数据。

3.7 send和sendto

#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);

send和sendto分别用于tcp和udp协议,参数含义和上面的recv类似,也可以使用write向socket中些数据。

四、一个简单的服务端和客户端案例

以下是一个示例demo,客户端在连接上服务端后输入相应的字符串发送过去,然后服务端把所有字符转成大写返回。

client.c

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

const int SERVER_PORT = 8080;
const int MAX_BUF_SIZE = 1024;
const char HOST[] = "127.0.0.1";

int main(int argc, char** argv){
    if (argc < 3){
        printf("Usage: ./client host port");
        return 0;
    }

    int clnt_fd, n;
    struct sockaddr_in serv_addr;
    char send_buf[MAX_BUF_SIZE];

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

    // set server addr
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[2]));
    inet_pton(AF_INET, argv[1], &serv_addr.sin_addr);

    if (connect(clnt_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){
        perror("connect error");
        return 0;
    }    

    printf("connect to server success!\n");

    int index = 0;
    while (index<100){
        bzero(send_buf, MAX_BUF_SIZE);

        scanf("%s", send_buf);
        if(strcmp("exit", send_buf) == 0){
            break;
        }

        n = write(clnt_fd, send_buf, strlen(send_buf));
        if (n == -1){
            perror("write error");
            return 0;
        }

        n = read(clnt_fd, send_buf, n);
        if (n == -1){
            perror("read error");
            return 0;
        }
        printf("%s\n", send_buf);
    }
    close(clnt_fd);

    return 0;
}

server.c

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

const int SERV_PORT = 8080;
const int MAX_CONN = 1;
const int MAX_BUF_SIZE = 1024;
const int MAX_IP_SIZE = 17;

void toUpper(char buf[]){
    int i = 0;
    while (i < MAX_BUF_SIZE && buf[i]){
        buf[i] = toupper(buf[i]);
        i++;
    }
}

int main(int argc, char** argv){

    if (argc < 2){
        printf("./server port\n");
        return 0;
    }

    int nPort = atoi(argv[1]);

    int clnt_fd, serv_fd;
    struct sockaddr_in clnt_addr, serv_addr;
    socklen_t len = sizeof(serv_addr);
    char buf[MAX_BUF_SIZE], ip[MAX_IP_SIZE];
    int n;

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

    bzero(&serv_addr, sizeof(serv_addr));
    bzero(&clnt_addr, sizeof(clnt_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(nPort);

    if (bind(serv_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){
        perror("Bind socket error");
        return 0;
    }

    if (listen(serv_fd, MAX_CONN) == -1){
        perror("Listen error");
        return 0;
    }

    printf("server hs start and listen %d\n", nPort);
    clnt_fd = accept(serv_fd, (struct sockaddr*)&serv_addr, &len);
    if (clnt_fd == -1){
        perror("Accpet error");
        return 0;
    }

    inet_ntop(AF_INET, &serv_addr.sin_addr, ip, sizeof(serv_addr));
    printf("Accept[%s:%d]\n", ip, serv_addr.sin_port);

    bzero(buf, MAX_BUF_SIZE);
    while((n = read(clnt_fd, buf, MAX_BUF_SIZE)) != 0){
        if (n == -1){
            perror("Read error");
            continue;
        }
        printf("Receive %d byte data: %s\n", n, buf);
        toUpper(buf);
        write(clnt_fd, buf, n);
        bzero(buf, MAX_BUF_SIZE);
    }

    printf("client[%s:%d] has disconnected!\n", ip, serv_addr.sin_port);
    close(clnt_fd);
    return 0;
}

运行结果

服务端通过./server 9988启动,监听9988端口。

客户端连接端口输入字符串测试:

> ./client 127.0.0.1 9988
connect to server success!
helloworld
HELLOWORLD
client
CLIENT
maqian
MAQIAN
exit # 输入exit退出

服务端显示:

root@ma:/data/code/c/2-socket/test# ./server 9988
server hs start and listen 9988
Accept[127.0.0.1:50336]
Receive 10 byte data: helloworld
Receive 6 byte data: client
Receive 6 byte data: maqian
client[127.0.0.1:50336] has disconnected!
最后修改:2019 年 04 月 20 日
如果觉得我的文章对你有用,请随意赞赏