Linux 系统编程学习笔记 - socket编程

预备知识

socket概念

socket可以表示很多概念:

  • 在TCP/IP协议中,“IP地址 + TCP/UDP端口号”唯一标识网络通讯中的一个进程,“IP地址+端口号”称为socket。
  • 在TCP协议中,建立连接的2个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。socket本身有“插座”的意思,因此用来描述连接的一对一关系。
  • TCP/IP协议最早在BSD UNIX上实现,为TCP/IP设计的应用层编程接口称为socket API。

网络编程中的socket, 通常指socket API (socket, bind, listen, accept, connect等)。

网络字节序

内存中多字节数据相对于内存地址,有大小端之分,网络数据流也有。

先看下什么是大端、小端?
详解大端模式和小端模式 | 博客园

以存放0x12345678为例,说明大端、小端的区别:

  1. 大端 big endian:高位字节存放在内存的低地址端,低位字节存放在内存的高地址端
    0x12345678本身 => 高位字节:0x12,低位字节:0x78 ,高位、低位字节取决于数据本身
低地址 --------------------> 高地址
0x12  |  0x34  |  0x56  |  0x78
  1. 小端 little endian:低位字节存放在内存的低地址端,高位字节存放在内存的高地址端
    0x12345678本身 => 高位字节:0x12,低位字节:0x78
低地址 --------------------> 高地址
0x78  |  0x56  |  0x34  |  0x12

网络数据流的地址
发送主机通常将发送缓冲区中的数据,按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存.
把地址的高低, 与字节序号的高低一一对应, 即地址低 <=> 字节序号低, 地址高 <=> 字节序号高.

网络数据流的大端小端是什么?
TCP/IP协议规定,网络数据流的采用大端字节序,即低地址高(位)字节。
下图是UDP协议数据包格式示意图:

下图以发送源端口号=0x3e8为例, 说明网络数据流的大端, 小端直接区别:

大端, 小端如何转换?
网络字节序是网络协议规定的, 而存储字节序取决于硬件体系结构. 它们如何转换?
如果发送和接收主机是大端字节序,就不需要转换;如果任何一端是小端字节序,就需要转换. 可以调用以下库函数做网络字节序和主机字节序转换(为了使网络程序具备可移植性,建议使用).
注: IP地址, 端口号可以利用下面的库函数转换.

#include <arpa/inet.h>

// h - host, n - network
// l - 32bit long, s - 16bit short
// 如果主机是小端字节序,将参数做相应的大小端转换,然后返回;
// 如果主机是大端字节序,参数直接返回
uint32_t htonl(uint32_t hostlong); // 32bit unsigned long主机序 -> 网络字节序
uint16_t htons(uit16_t hostshort); // 16bit unsigned short主机序 -> 网络字节序
uint32_t ntohl(uint32_t netlong);  // 32bit unsigned short网络字节序 -> 主机序
uint16_t ntohs(uint16_t netshort); // 16bit unsigned short网络字节序 -> 主机序

socket地址的数据类型及相关函数

socket API是一层抽象的网络编程接口,适用于各种底层网络协议(如Ipv4、Ipv6及UNIX Domain Socket)。然而各种网络协议的地址格式并不相同:
IPv4地址格式 , 地址类型; IPv6地址格式 struct sockaddr_in6; UNIX Domain Socket地址格式 struct sockaddr_un

地址格式(socket addr类型) 地址类型/协议族(sa_family_t) 备注
IPv4 struct sockaddr_in AF_INET 用于IPv4通信
IPv6 struct sockaddr_in6 AF_INET6 用于IPv6通信
Unix Domain Socket struct sockaddr_un AF_INET6 用于Unix Domain Socket通信
注: 头文件 netinet/in.h

sockaddr数据结构示意图:

问题:从上面的表格知道, 可以从结构体类型上区分是使用IPv4, IPv6 or Unix Domain Socket,为何还要多添加一个字段sa_family_t用于表示地址类型呢?
一种地址类型(协议族)只有一种地址格式与之对应, 如AF_INET表示IPv4通信, 只有struct sockaddr_in对应的IPv4数据报文格式, 才能与之对应. 为了统一接口, 无论IPv4, IPv6, 还是Unixi Domain Socket, 都使用通用的struct sockaddr格式, 这样socket API可以接受各种类型的sockaddr结构体指针做参数,而不需要知道具体是哪种类型, 例如bind, accept, connect等函数的参数可以用同一种类型struct sockaddr*来表示地址格式. 具体的哪种类型的地址, 就用地址类型来区分.
传递参数示例(需要强制类型转换):

struct sockaddr_in sockaddr; // IPv4地址格式, 包含了IP地址 + 端口信息
... // 设置sockaddr的IP地址, 端口. 发送数据注意使用网络序, 接收数据使用主机序

// 绑定套接字与IP地址 + 端口信息 
// servaddr对应实参可能是struct sockaddr_in, struct sockaddr_in6, 或struct sockaddr_un
// 如果使用具体的地址格式类型, 有多少种不同协议地址类型, 就需多少种接口
bind(listen_fd, (struct_sockaddr *)&sockaddr, sizeof(sockaddr)); 

IP地址转换 字符串 <-> 32bit数据
IPv4的sockaddr_in的成员sin_addr (struct in_addr)表示32位IP地址, 常需要转化成点分十进制的字符串IP地址(const char *类型).

点分十进制字符串 -> in_addr:

#include <arpa/inet.h>

/* strptr 点分十进制IP地址;
* addrptr 32bit ip地址
*/
int inet_aton(const char *strptr, struct in_addr *addrptr);
int_addr inet_addr(const char *strptr);
// 支持转换IPv4的in_addr,也支持IPv6的in6_addr
int inet_pton(int family, const char *strptr, void *addrptr); 

in_addr转点分十进制字符串:

char *inet_ntoa(struct in_addr inaddr);
// 支持转换IPv4的in_addr,也支持IPv6的in6_addr
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

基于TCP协议的网络程序

TCP client/server程序一般流程:

时序图:

连接建立的程序过程

  1. 服务器调用socket(), bind(), listen() 进行初始化;
  2. 服务器调用accept()阻塞等待,监听端口状态;
  3. 客户端调用socket()初始化;
  4. 客户端调用connect()发送SYN段,并阻塞等待服务器应答;
  5. 服务器收到SYN段后,应答一个SYN-ACK段;
  6. 客户端收到服务器的SYN-ACK段后,从已阻塞的connect()返回,同时应答一个ACK段;
  7. 服务器收到客户端应答的ACK段后,从已阻塞的accept返回。

数据传输的过程

  1. 服务器从accept()返回后立刻调用read(),读socket,如果没有数据就阻塞等待;
  2. 客户端调用write()发送请求给服务器,服务器收到后从read()返回,并对客户端的请求进行处理;
  3. 客户端write()之后,调用read()阻塞等待服务器的应答;
  4. 服务器wirte()之后,调用read()阻塞等待下一条请求,客户端读取后从read()返回,发送下一条请求(跳到2),这样不断循环。

连接的断开过程

  1. 如果客户端没有更多请求了,调用close()关闭连接;
  2. 服务器从read()返回0,就得知客户端关闭了连接,也调用close()关闭连接。
    注意:任何一方调用close(),连接的两个传输方向都关闭,不能再发送数据。如果一方调用shutdown则连接处于半关闭状态,仍可接收对方发来的数据。

最简单的TCP网络程序

完整代码参见: linuxstudy 文件名:server.c, client.c

server端

例,server.c的作用是从client读字符,然后将每个字符转换为大写并回送到客户端。

// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <stdbool.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE   80
#define SERV_PORT 8000

int main() {
  struct sockaddr_in servaddr, cliaddr;
  socklen_t cliaddr_len;
  int listenfd, connfd;
  char buf[MAXLINE];
  char str[INET_ADDRSTRLEN];  // 16
  int i, n;

  // step1 socket() 初始化socket,获取文件描述符
  // 类似于打开一个通道, listenfd就是这个通道的文件描述符
  listenfd = socket(AF_INET, SOCK_STREAM, 0); // AF_INET - IPv4 IP地址;AF_INET6 - IPv6 IP地址;AF_UNIX - Unix Domain Socket
  
  // 初始化sockaddr, 设置socket IP地址+端口信息
  bzero(&servaddr, sizeof(servaddr)); // 清空sockaddr
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 将32位主机序 IP地址INADDR_ANY 转化成网络序IP地址 
  servaddr.sin_port = htons(SERV_PORT); // 将16位主机序 端口号SERV_PORT 转换成网络序端口号
  /*
  思考:这里为什么要用htonl/htons转换IP地址和端口?为什么不转地址类型AF_INET?
  因为地址类型AF_INET是用于程序判断IP地址类型的,而IP地址和端口是要用网卡发送到网络上的数据,为了确保程序可移植性,需要转换成网络字节序
  */
  
  // step2 bind() 绑定socket和IP地址
  bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 注意bind第二个参数接收的是struct sockaddr *类型,而变量sockaddr是sockaddr_in类型
  
  // step3 listen() 设置监听队列
  listen(listenfd, 20); // 处于监听状态的的流套接字listenfd, 将维护一个客户请求队列, 最多容纳20个用户请求

  printf("Accepting connections...\n");
  while(true) {
    cliaddr_len = sizeof(cliaddr);
    
    // step4 accept() 阻塞等待客户端连接请求
    connfd = accept(listenfd, &cliaddr, &cliaddr_len); // 阻塞等待客户连接请求(SYN段),如果从accept成功返回表明已经收到客户端SYN段
    // step5 read()/write() 读取客户端数据或向客户端发送数据
    // 为什么是从accept返回的connfd读取,而不是socket返回的listenfd?因为accept绑定的是客户端,listenfd绑定的是服务器端(也就是自身)的IP地址及端口信息
    n = read(connfd, buf, MAXLINE); // 从监听端口读取n byte数据,存储到buf[0..n-1]
    printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),  // 将k客户端IP地址信息in_addr转换成字符串str,
      ntohs(cliaddr.sin_port)); // 将客户端端口号由主机字节序转化成网络字节序

    for (i = 0; i < n; ++i) {
      buf[i] = toupper(buf[i]); // buf[0..n-1]转换成大写
    }
    write(connfd, buf, n); // 写回给客户端

    // step6 close() 发送FIN段,关闭TCP连接
    close(connfd);
  }
  
  return 0;
}

socket(), bind(), listen(), accept(), read(), write() 这几个socket API都位于头文件sys/socket.h中。

  • socket()
    socket()打开一个网络通讯端口,如果成功,就像open一样返回一个文件描述符,可以通过该文件描述符绑定IP地址、监听端口、与客户端建立连接。
    要真正让APP可以像读写文件一样read()/write()在网络上收发数据,还需要绑定IP地址及端口,还有和客户端建立连接之后。
#include <sys/socket.h>
#include <sys/types.h>

// 成功返回一个文件描述符;出错返回-1
// family 用于指示协议族的名字,AF_INET为IPv4,AF_INET6为IPv6,AF_UNIX为Unix Domain Socket
// type 指示类型,SOCK_STREAM表示TCP协议,SOCK_DGRAM表示UDP协议
// protocol 用于指示对于这种socket的具体协议类型。一般情况下,指定了前2个参数就,如果只存在一种协议类型对应该情况,就可以将protocol设置为0;某些情况下,会存在多个协议类型时,就必须指定具体的协议类型。
int socket(int family, int type, int protocol);

SOCK_STREAM(流) : 提供有序,可靠的双向连接字节流。 可以支持带外数据传输机制,无论多大的数据都不会截断;
SOCK_DGRAM(数据报):支持数据报(固定最大长度的无连接,不可靠的消息),数据报超过最大长度,会被截断;

  • bind()
    服务器监听的网络地址和端口号通常是固定不变的(通常是知名服务, 如Telnet/STMP等, 对应着知名端口号), 客户端程序得知服务器程序的地址和端口号后,可以向服务器发起连接请求.
    服务器调用bind绑定一个固定的网络地址和端口号; 客户端IP和端口不固定,不需要bind绑定端口, 也就是不需要调用bind.
// 绑定sockfd和myaddr
// addrlen 绑定地址从长度
// 为什么需要第3个参数addrlen? 
// 事实上,myaddr可以接受多种协议的sockaddr结构体,而它们参数各不相同,所以需要addrlen指定结构体长度
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

程序中为什么要将地址类型设为INADDR_ANY?
因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以监听所有本地IP地址,直到与某个客户端建立了连接才确定到底用哪个IP地址

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY转换过来, 0.0.0.0, 泛指本机IP地址, 可以使用一套代码同时监听多个网卡
servaddr.sin_port = htons(SERV_PORT);  // SERV_PORT = 8000
  • listen()
    服务器一般可以服务多个客户端,如果有大量客户端同时发起连接,服务器可能会来不及处理,尚未accept处理的客户端处于等待状态。listen可以让服务器为sockfd维护一个队列,监听客户端连接请求。
// 监听端口
// sockfd 用socket()成功创建的TCP套接字(文件描述符)
// backlog 监听队列的大小,即最多容许backlog个客户端处于等待连接状态
// 成功返回0,失败-1
int listen(int sockfd, int backlog);
  • accept()
    accept用于从指定套接字的连接队列中取出第一个连接,并返回一个新的套接字用于与客户端通信。
    accept会一直阻塞,直到有客户端成功连接(已完成三次握手)
    客户端是连接的请求方,而不是接受方,不需要调用accept。
// sockaddr 处于监听状态的套接字
// cliaddr 用于保存客户端的地址信息
// addrlen_t [in][out] 传入传出参数,传入值是cliaddr缓存的大小以避免溢出,传出值是客户端地址结构体的实际长度。 NULL表示不关心客户端的地址
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen_t);

服务器程序结构:

while(true) {
  cliaddr_len = sizeof(cliaddr);
  connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
  n = read(connfd, buf, MAXLINE);
  ...
  close();
}

这里的read, write跟文件IO里面的读写操作,是同一个IO接口,位于头文件unistd.h

client端

client.c从命令行获取一个字符串,发给服务器,然后接收服务器返回的字符串并打印。

// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h> 

#define MAXLINE    80
#define SERV_PORT  8000

int main(int argc, char *argv[]) {
  struct socketaddr_in sockaddr;
  char buf[MAXLINE];
  int sockfd, n;
  char *str;

  if (argc != 2) {
    fputs("usage: ./client message\n", stderr);
    exit(1);
  }
  
  str = argv[1]; // 要传给服务器的字符串
  
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  bzero(&sockaddr, sizeof sockaddr);
  sockaddr.sin_family = AF_INET;
  sockaddr.sin_port = htons(SERV_PORT);
  inet_pton(AF_INET, "127.0.0.1", &sockaddr.sin_addr);
  
  connect(sockfd, (struct sockaddr *)&sockaddr, sizeof sockaddr);

  write(sockfd, str, strlen(str));
  n = read(sockfd, buf, MAXLINE);
  printf("Response from Server:\n");
  write(STDOUT_FILENO, buf, n);
  
  close(sockfd);
  return 0;
}

先编译、运行服务器:

$ gcc server.c -o server
$ ./server

查看服务器端口占用情况:

$ netstat -apn|grep 8000

服务器占用8000端口,但IP地址还未确定

另开终端,编译运行客户端:

$ gcc client.c -o client
$ ./client abcd
Response from Server:
ABCD

回到server终端,可以看到server输出:

$ ./server
Accepting connections...
received from 127.0.0.1 at PORT xxx

FAQ
如何处理多个client请求?
不同client连接请求,可以用listen设置监听队列大小来缓存,但是对于已经连接上的client,如何并发处理?

  1. 方式一:使用fork并发处理。阻塞IO
    每accept一个客户连接请求,就fork一个子进程来负责read/write以及close。

2.方式二:使用select()同时监听多个阻塞文件的文件描述符(套接字),哪个有数据达到就处理哪个,不需要fork和多进程。 相当于IO多路复用。
参考linux select函数解析以及事例 | 知乎select()函数以及FD_ZERO、FD_SET、FD_CLR、FD_ISSET

如何解决client进程未终止,但服务器程序重启,端口被占用的问题?
服务器重启或退出时,会主动关闭连接,服务器发送FIN段给客户端,客户端收到FIN后处于CLOSE_WAIT状态,但client没有终止,也没有关闭socket 描述符(TCP套接字),因此不会发送FIN给服务器,因此server的TCP链接处于FIN_WAIT2状态。

bind error: Address already in use

解决办法:使用setsockopt()设置socket描述符选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。具体来说是在socket()和bind()之间插入如下代码:

int op = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);

基于UDP协议的网络程序

典型的UDP通讯流程

UDP和TCP的主要区别:

  1. UDP是面向消息的,无连接的,连接不可靠,TCP是面向字节流的,要传输数据必须先进行连接(三次握手),连接可靠;
  2. IP数据报文格式不一样,TCP对应IP报文首部包含了SYN、WIN、ACK、FIN等与连接相关的信息,UDP的IP报文首部则不包含这些;
  3. 程序上,UDP在创建socket文件描述符后,服务器端只需进行bind IP地址信息,无需listen、accept等与监听、连接有关的操作,客户端也无需connect进行连接请求;

一个简单的UDP服务器和客户端程序示例
server.c

// server.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>

#include "wrap.h"

#define MAXLINE   80
#define SERV_PORT 8000

int main() {
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;
    int sockfd;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN]; // 16
    int i, n;

    // socket() choose protocol - IPv4 UDP
    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

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

    // bind() bind ip info
    Bind(sockfd, (struct sockaddr *)&servaddr, sizeof servaddr);

    printf("Accepting connections ...\n");

    while(true) {
        cliaddr_len = sizeof cliaddr;
        n = recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&cliaddr, &cliaddr_len);
        if (n == -1)
            perr_exit("recvfrom error");

        printf("received from %s at PORT %d\n",
               inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof str), // in_addr to string
               ntohs(cliaddr.sin_port)
        );

        for (i = 0; i < n; ++i) {
            buf[i] = toupper(buf[i]);
        }

        n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, cliaddr_len);

        if (n == -1) perr_exit("sendto error");
    }

    return 0;
}

client.c

// client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>

#include "wrap.h"

#define MAXLINE   80
#define SERV_PORT 8000

int main() {
    struct sockaddr_in servaddr;
    int sockfd, n;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN]; // 16
    socklen_t servaddr_len;

    // socket() choose UDP protocol
    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    bzero(&servaddr, sizeof servaddr);
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

    while (fgets(buf, MAXLINE, stdin) != NULL) {
        n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof servaddr);
        if (n == -1) perr_exit("sendto error");

        n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0);
        if (n == -1) perr_exit("recvfrom error");
        write(STDOUT_FILENO, buf, n);
    }

    Close(sockfd);
    return 0;
}

UNIX Domain Socket IPC

参考Linux下进程间通讯方式 - UNIX Domain Socket

服务端: socket -> bind -> listen -> accet -> recv/send -> close
客户端: socket -> connect -> recv/send -> close
当然,客户端也可以bind,自己指定socket文件,便于服务器区分不同的客户端。

服务器端
使用listen设置监听队列时,socket()的type参数不能使用SOCK_DGRAM,需使用SOCK_STREAM。

// server.c
#include <stdlib.h>
#include <stdio.h>
#include <stddef.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <ctype.h>
#include <stdbool.h>

#define QLEN  2
#define MAXLINE 80

/**
 * create a server endpoint of a connection
 * @param name
 * @return Returns fd if all OK, <0 on error.
 */
int serv_listen(const char *name) {
    int fd, len, err, rval;
    struct sockaddr_un un;

    if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
        return -1;
    unlink(name); // remove the special file if it exists
    printf("unlink name is %s\n", name);

    // fill in socket address structure
    memset(&un, 0, sizeof un);
    un.sun_family = AF_UNIX;
    strcpy(un.sun_path, name);

    printf("server sun_path is %s\n", un.sun_path);

    int opt = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt);

    len = offsetof(struct sockaddr_un, sun_path) + strlen(name);

    // bind the name to descriptor
    if (bind(fd, (struct sockaddr *)&un, len) < 0) {
//    if (bind(fd, (struct sockaddr *)&un, sizeof un) < 0) {
        rval = -2;
        goto errout;
    }
    if (listen(fd, QLEN) < 0) {
        printf("serv_listen -3\n");
        rval = -3;
        goto errout;
    }

    return fd;

errout:
    err = errno;
    close(fd);
    errno = err;
    return rval;
}

int serv_accept(int listenfd, uid_t *uidptr) {
    int clifd, len, err, rval;
    struct sockaddr_un un;
    struct stat statbuf;

    len = sizeof un;
    if ((clifd = accept(listenfd, (struct  sockaddr *)&un, &len)) < 0)
        return -1;

    len -= offsetof(struct sockaddr_un, sun_path); // length of pathname

    if (stat(un.sun_path, &statbuf) < 0) {
        rval = -2;
        goto errout;
    }

    if (S_ISSOCK(statbuf.st_mode) == 0) {
        rval = -3;
        goto errout;
    }

    if (uidptr != NULL)
        *uidptr = statbuf.st_uid;
    unlink(un.sun_path);
    return clifd;

errout:
    err = errno;
    close(clifd);
    return rval;
}

int main() {
    int servfd, clifd;
    char *name = "foo.socket";
    uid_t uid;
    char buf[MAXLINE];
    int i;

    servfd = serv_listen(name);
    if (servfd == -1) {
        perror("socket error\n");
        exit(1);
    }
    else if (servfd == -2) {
        perror("bind error\n");
        exit(2);
    }
    else if (servfd == -3) {
        perror("listen error\n");
        exit(3);
    }
    else printf("listen successfully\n");
    for (; ; ) {
        clifd = serv_accept(servfd, &uid);
        if (clifd < 0) {
            perror("serv_accept error");
            exit(4);
        }

        while (true) {
            int ret = recv(clifd, buf, MAXLINE, 0);
            if (ret < 0) {
                printf("recv error\n");
                break;
            }

            printf("received: %s size = %ld\n", buf, strlen(buf));

            for (i = 0; i < MAXLINE; ++i) {
                buf[i] = toupper(buf[i]);
            }
            send(clifd, buf, ret, 0);
        }
        close(clifd);
    }

    close(servfd);
    return 0;
}

客户端
使用bind的socket文件名不能与服务器端的同名(同路径+同文件名),而connect的socket文件名需要与服务器端的socket文件同名;

// client.c
#include <stdlib.h>
#include <stdio.h>
#include <stddef.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>

#define MAXLINE  80
#define CLIA_PATH  "/var/tmp/"  // + 5 for pid = 14 chars

int cli_conn(const char *name) {
    int fd, len, err, rval;
    struct sockaddr_un un;

    // create a UNIX domain socket
    if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
        perror("socket error\n");
        return -1;
    }

    memset(&un, 0, sizeof un);
    un.sun_family = AF_UNIX;
    sprintf(un.sun_path, "%s%05d", CLIA_PATH, getpid());
    len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);

    unlink(un.sun_path);
    puts(un.sun_path);

//    if (bind(fd, (struct sockaddr *)&un, sizeof un)) {
    if (bind(fd, (struct sockaddr *)&un, len) < 0) {
        perror("bind error\n");
        rval = -2;
        goto errout;
    }

    // fill socket address structure with server's address
    memset(&un, 0, sizeof un);
    un.sun_family = AF_UNIX;
    strcpy(un.sun_path, name); // server create socket file with the same
    len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
    if (connect(fd, (struct sockaddr*)&un, len) < 0) {
        perror("connect error...\n");
        rval = -4;
        goto errout;
    }
    return fd;

    errout:
    err = errno;
    close(fd);
    errno = err;
    return rval;
}

int main() {
    int sockfd;
    char* name = "foo.socket";
    char buf[MAXLINE];

    sockfd= cli_conn(name);
    if (sockfd < 0) {
        perror("connect error\n");
        exit(1);
    }
    printf("connect successfully\n");

    while (fgets(buf, MAXLINE, stdin) != NULL) {
        int n = send(sockfd, buf, strlen(buf), 0);
        if (n < 0) {
            perror("send error\n");
            exit(1);
        }
        else if (n == 0) {
            printf("send 0 byte\n");
            break;
        }

        int ret = recv(sockfd, buf,MAXLINE, 0);
        write(STDOUT_FILENO, buf, ret);
    }
    printf("terminate\n");

    return 0;
}

参考

《linuxC编程一站式学习》

posted @ 2021-04-07 16:22  明明1109  阅读(272)  评论(0编辑  收藏  举报