2. 套接字

套接字(Socket)是Linux下跨主机、跨进程的核心通信机制,既支持本地单设备进程通信, 也能实现网络中不同设备的进程数据交互,弥补了System V IPC仅能本地通信的局限,是TCP/IP网络编程的基础。

2.1. Socket基本概念

套接字(socket)是一种通信机制,凭借这种机制, 客户端<->服务器 模型的通信方式既可以在本地设备上进行,也可以跨网络进行。 套接字的创建和使用与管道是有区别的,因为套接字明确地将客户端、服务器区分开来,而且套接字机制可以实现将多个客户连接到一个服务器。

Socket英文原意是“孔”或者“插座”的意思,在网络编程中,通常将其称之为“套接字”,当前网络中的主流程序设计都是使用Socket进行编程的,因为它简单易用, 它还是一个标准(BSD Socket),能在不同平台很方便移植,比如你的一个应用程序是基于Socket编程的,那么它可以移植到任何实现BSD Socket标准的平台, 比如LwIP,它兼容BSD Socket;又比如Windows,它也实现了一套基于Socket的套接字接口,更甚至在国产操作系统中,如RT-Thread,它也实现了BSD Socket标准的Socket接口。

在Socket中,它使用一个套接字来记录网络的一个连接,套接字是一个整数,就像我们操作文件一样,利用一个文件描述符,可以对它打开、读、写、关闭等操作, 类似的,在网络中,我们也可以对Socket套接字进行这样子的操作,比如开启一个网络的连接、读取连接主机发送来的数据、向连接的主机发送数据、终止连接等操作。

2.2. Socket分类与协议选型

Socket通信依托协议族和服务类型区分场景,开发前需精准选型,避免协议适配错误。

2.2.1. 协议族

  • AF_UNIX/AF_LOCAL:本地进程通信,无需网络,效率极高,适合同设备进程交互;

  • AF_INET:IPv4协议族,主流网络通信选型,适配绝大多数网络场景;

  • AF_INET6:IPv6协议族,适配下一代IP网络,解决IPv4地址不足问题。

2.2.2. 服务类型与协议匹配

Socket服务类型核心对比

服务类型

对应协议

核心特性

适用场景

SOCK_STREAM

TCP

面向连接、可靠传输、字节流、无数据边界、全双工

文件传输、即时通讯、网页请求等需可靠通信场景

SOCK_DGRAM

UDP

无连接、不可靠、数据报、有数据边界、开销小

视频直播、广播通信、实时数据传输等容忍少量丢包场景

SOCK_RAW

原始IP

底层封包、可自定义协议头

网络抓包、协议开发、网络工具研发

提示

TCP是流式协议,无数据边界,易出现粘包问题;UDP是数据报协议,单次发送接收一一对应,无粘包但存在丢包、乱序风险,开发需针对性处理。

2.3. 套接字函数说明

Socket编程需包含头文件:<sys/types.h>、<sys/socket.h>、<netinet/in.h>、<arpa/inet.h>, 所有API成功返回0/合法描述符,失败返回-1并设置errno。

2.3.1. socket函数

socket()函数用于创建一个socket描述符(socket descriptor),它唯一标识一个socket,这个socket描述符跟文件描述符一样, 后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

函数原型:

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

创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

  1. domain:参数domain表示该套接字使用的协议族,在Linux系统中支持多种协议族,对于TCP/IP协议来说,选择AF_INET就足以,当然如果你的IP协议的版本支持IPv6,那么可以选择AF_INET6,可选的协议族具体见:

  • AF_UNIX, AF_LOCAL: 本地通信

  • AF_INET : IPv4

  • AF_INET6 : IPv6

  • AF_IPX : IPX - Novell 协议

  • AF_NETLINK : 内核用户界面设备

  • AF_X25 : ITU-T X.25 / ISO-8208 协议

  • AF_AX25 : 业余无线电 AX.25 协议

  • AF_ATMPVC : 访问原始ATM PVC

  • AF_APPLETALK : AppleTalk

  • AF_PACKET : 底层数据包接口

  • AF_ALG : 内核加密API的AF_ALG接口

  1. type:参数type指定了套接字使用的服务类型,可能的类型有以下几种:

  • SOCK_STREAM:提供可靠的(即能保证数据正确传送到对方)面向连接的Socket服务,多用于资料(如文件)传输,如TCP协议。

  • SOCK_DGRAM:是提供无保障的面向消息的Socket 服务,主要用于在网络上发广播信息,如UDP协议,提供无连接不可靠的数据报交付服务。

  • SOCK_SEQPACKET:为固定最大长度的数据报提供有序的,可靠的,基于双向连接的数据传输路径。

  • SOCK_RAW:表示原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用得比较少,暂时不用理会它。

  • SOCK_RDM:提供不保证排序的可靠数据报层。

  1. protocol:参数protocol指定了套接字使用的协议,在IPv4中,只有TCP协议提供SOCK_STREAM这种可靠的服务,只有UDP协议提供SOCK_DGRAM服务,对于这两种协议,protocol的值均为0,因为当protocol为0时,会自动选择type类型对应的默认协议。

当创建套接字成功的时候,该函数返回一个int类型的值,也就是socket描述符,该值大于等于0;而如果创建套接字失败时则返回-1。

2.3.2. bind函数

bind函数用于将一个IP地址或端口号与一个套接字进行绑定。

函数原型:

1
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

在套接口中,一个套接字只是用户程序与内核交互信息的枢纽,它自身没有太多的信息,也没有网络协议地址和端口号等信息, 在进行网络通信的时候,必须把一个套接字与一个IP地址或端口号相关联,这个过程就是绑定的过程。

提示

bind()函数并不是总是需要调用的,只有用户进程想与一个具体的地址或端口相关联的时候才需要调用这个函数。如果用户进程没有这个需要,那么程序可以依赖内核的自动的选址机制来完成自动地址选择。

参数:

  • sockfd:sockfd是由socket()函数返回的套接字描述符。

  • my_addr:my_addr是一个指向套接字地址结构的指针。

  • addrlen:addrlen指定了以addr所指向的地址结构体的字节长度。

若bind()函数绑定成功则返回0,若出错则为-1。

sockaddr 结构体内容如下:

sockaddr 结构体:

1
2
3
4
struct sockaddr {
    sa_family_t     sa_family;
    char            sa_data[14];
}

咋一看这个结构体,好像没啥信息要我们填写的,确实也是这样子,我们需要填写的IP地址与端口号等信息,都在sa_data连续的14字节信息里面, 但这个结构体对用户操作不友好,一般我们在使用的时候都会使用sockaddr_in结构体,sockaddr_in和sockaddr是并列的结构(占用的空间是一样的), 指向sockaddr_in的结构体的指针也可以指向sockadd的结构体,并代替它,而且sockaddr_in结构体对用户将更加友好,在使用的时候进行类型转换就可以了。

sockaddr_in结构体:

1
2
3
4
5
6
struct sockaddr_in {
    short int sin_family;               /* 协议族 */
    unsigned short int sin_port;        /* 端口号 */
    struct in_addr sin_addr;            /* IP地址 */
    unsigned char sin_zero[8];          /* sin_zero是为了让sockaddr与sockaddr_in两个数据结构体保持大小相同而保留的空字节 */
};

sockaddr_in结构体的第一个字段是与sockaddr结构体是一致的,而剩下的字段就是sa_data连续的14字节信息里面的内容, 只不过重新定义了成员变量而已,sin_port字段是我们需要填写的端口号信息,sin_addr字段是我们需要填写的IP地址信息, 剩下sin_zero区域的8字节保留未用。

举个简单的使用实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct sockaddr_in server;

bzero(&server, sizeof(server));

// assign IP, PORT
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_ANY);
server.sin_port = htons(6666);

// binding newly created socket to given IP and verification
bind(sockfd, (struct sockaddr*)&server, sizeof(server));

2.3.3. connect函数

connect函数用于客户端中,将sockfd与远端IP地址、端口号进行绑定,在TCP客户端中调用这个函数将发生握手过程(会发送一个TCP连接请求), 并最终建立一个TCP连接,而对于UDP协议来说,调用这个函数只是在sockfd中记录远端IP地址与端口号,而不发送任何数据,参数信息与bind()函数是一样的。

函数原型:

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

函数调用成功则返回0,失败返回-1,错误原因存于errno中。

connect()函数是套接字连接操作,对于TCP协议来说,connect()函数操作成功之后代表对应的套接字已与远端主机建立了连接,可以发送与接收数据。

对于UDP协议来说,没有连接的概念,在这里可将其描述为记录远端主机的IP地址与端口号,UDP协议经过connect()函数调用成功之后, 在通过sendto()函数发送数据报时不需要指定目的地址、端口,因为此时已经记录到了远端主机的IP地址与端口号。

UDP协议还可以给同一个套接字进行多次connect()操作,而TCP协议不可以,TCP只能指定一次connect操作。

2.3.4. listen函数

listen函数只能在TCP服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求, listen函数在一般在bind函数之后调用,在accept函数之前调用,它的函数原型是:

1
int listen(int s, int backlog);

参数:

  • sockfd:sockfd是由socket()函数返回的套接字描述符。

  • backlog:用来描述sockfd的等待连接队列能够达到的最大值。

2.3.5. accept函数

accept函数用于TCP服务器中,等待着远端主机的连接请求,并且建立一个新的TCP连接。

函数原型:

1
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

为了能够正常让TCP客户端能正常连接到服务器,服务器必须遵循以下流程处理:

  1. 调用socket()函数创建对应的套接字类型。

  2. 调用bind()函数将套接字绑定到本地的一个端口地址。

  3. 调用listen()函数让服务器进程进入监听状态,等待客户端的连接请求。

  4. 调用accept()函数处理到来的连接请求。

在调用这个函数之前需要通过调用listen()函数让服务器进入监听状态,如果队列中没有未完成连接套接字, 并且套接字没有标记为非阻塞模式,accept()函数的调用会阻塞应用程序直至与远程主机建立TCP连接;

参数addr用来返回已连接的客户端的IP地址与端口号,参数addrlen用于返回addr所指向的地址结构体的字节长度, 如果我们对客户端的IP地址与端口号不感兴趣,可以把arrd和addrlen均置为空指针。

若连接成功则返回一个socket描述符(非负值),若出错则为-1。

提示

如果accept()连接成功,那么其返回值是由内核自动生成的一个全新描述符,代表与客户端的TCP连接,一个服务器通常仅仅创建一个监听套接字,它在该服务器生命周期内一直存在,内核为每个由服务器进程接受的客户端连接创建一个已连接套接字。

2.3.6. read函数

一旦客户端与服务器建立好TCP连接之后,我们就可以通过sockfd套接字描述符来收发数据, 这与我们读写文件是差不多的操作,接收网络中的数据函数可以是read()、recv()、recvfrom()等。

函数原型:

1
2
3
4
5
6
7
8
ssize_t read(int fd, void *buf, size_t count);

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

// ssize_t 表示的是 signed size_t 类型

read()从描述符fd中读取count字节的数据并放入从buf开始的缓冲区中,read()函数调用成功返回读取到的字节数, 此返回值受文件剩余字节数限制,当返回值小于指定的字节数时并不意味着错误; 这可能是因为当前可读取的字节数小于指定的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0。

参数:
  • fd:在socket编程中是指定套接字描述符。

  • buf:指定存放数据的地址。

  • count:是指定读取的字节数,将读取到的数据保存在缓冲区buf中。

错误代码:
  • EINTR:在读取到数据前被信号所中断。

  • EAGAIN:使用O_NONBLOCK 标志指定了非阻塞式输入输出,但当前没有数据可读。

  • EIO:输入输出错误,可能是正处于后台进程组进程试图读取其控制终端,但读操作无效,或者被信号SIGTTIN所阻塞, 或者其进程组是孤儿进程组,也可能执行的是读磁盘或者磁带机这样的底层输入输出错误。

  • EISDIR:fd 指向一个目录。

  • EBADF:fd不是一个合法的套接字描述符,或者不是为读操作而打开。

  • EINVAL:fd所连接的对象不可读。

  • EFAULT:buf 超出用户可访问的地址空间。

2.3.7. recv函数

不论是客户端还是服务器应用程序都可以用recv()函数从TCP连接的另一端接收数据,它与read()函数的功能是差不多的。

函数原型:

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

recv()函数会先检查套接字 s 的接收缓冲区,如果 s 接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv()函数就把 s 的接收缓冲中的数据拷贝到 buf 中,但是要注意的是议接收到的数据可能大于buf的长度, 所以在这种情况下要调用几次recv()函数才能把s的接收缓冲中的数据拷贝完。

recv()函数仅仅是拷贝数据,真正的接收数据是由协议来完成的,recv函数返回其实际拷贝的字节数。 如果recv()函数在拷贝时出错,那么它返回SOCKET_ERROR;如果recv()函数在等待协议接收数据时网络中断了,那么它返回0。

参数:

  • sockfd:指定接收端套接字描述符。

  • buf:指定一个接收数据的缓冲区,该缓冲区用来存放recv()函数接收到的数据。

  • len:指定recv()函数拷贝的数据长度。

参数 flags 一般设置为0即可,其他数值定义如下:

  • MSG_OOB:接收以out-of-band送出的数据。

  • MSG_PEEK:保持原有数据,就是说接收到的数据并不会被删除,如果再调用recv()函数还会拷贝相同的数据到buf中。

  • MSG_WAITALL:强迫接收到指定len大小的数据后才能返回,除非有错误或信号产生。

  • MSG_NOSIGNAL:recv()函数不会被SIGPIPE信号中断,返回值成功则返回接收到的字符数,失败返回-1,错误原因存于errno中。

错误代码:

  • EBADF:fd 不是一个合法的套接字描述符,或者不是为读操作而打开。

  • EFAULT:buf 超出用户可访问的地址空间。

  • ENOTSOCK:参数 s 为一文件描述词, 非socket.

  • EINTR:在读取到数据前被信号所中断。

  • EAGAIN:此动作会令进程阻塞, 但参数s的 socket 为不可阻塞。

  • ENOBUFS:buf内存空间不足。

  • ENOMEM:内存不足。

  • EINVAL:传入的参数不正确。

2.3.8. write函数

write()函数一般用于处于稳定的TCP连接中传输数据,当然也能用于UDP协议中,它向套接字描述符 fd 中写入 count 字节的数据,数据起始地址由 buf 指定,函数调用成功返回写的字节数,失败返回-1,并设置errno变量。

1
ssize_t write(int fd, const void *buf, size_t count);

在网络编程中,当我们向套接字描述符写数据时有两种可能:

  1. write()函数的返回值大于0,表示写了部分数据或者是全部的数据,这样我们可以使用一个while循环不断的写入数据,但是循环过程中的 buf 参数和 count 参数是我们自己来更新的,也就是说,网络编程中写函数是不负责将全部数据写完之后再返回的,说不定中途就返回了!

  2. 返回值小于0,此时出错了,需要根据错误类型进行相应的处理。

所以一般我们处理写数据的时候都会自己封装一层,以保证数据的正确写入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Write "n" bytes to a descriptor. */
ssize_t writen(int fd, const void *vptr, size_t n)
{
    size_t      nleft;      //剩余要写的字节数
    ssize_t     nwritten;   //已经写的字节数
    const char  *ptr;       //write的缓冲区

    ptr = vptr;             //把传参进来的write要写的缓冲区备份一份
    nleft = n;              //还剩余需要写的字节数初始化为总共需要写的字节数

    //检查传参进来的需要写的字节数的有效性
    while (nleft > 0) {
        if ( (nwritten = write(fd, ptr, nleft)) <= 0) { //把ptr写入fd
            if (nwritten < 0 && errno == EINTR) //当write返回值小于0且因为是被信号打断
                nwritten = 0;       /* and call write() again */
            else
                return(-1);         /* error 其他小于0的情况为错误*/
        }

        nleft -= nwritten;          //还剩余需要写的字节数=现在还剩余需要写的字节数-这次已经写的字节数
        ptr += nwritten;          //下次开始写的缓冲区位置=缓冲区现在的位置右移已经写了的字节数大小
    }
    return(n); //返回已经写了的字节数
}

提示

如果是比较简单的数据(比如单行数据)倒是不需要那么麻烦,直接调用write()也是完全没有问题的,只是看情况写代码就行了,上面代码的封装只是保证程序的健壮性。

注意

这个函数在写入数据完成后并不是立即发送的,至于什么时候发送则由TCP/IP协议栈决定。

2.3.9. send函数

无论是客户端还是服务器应用程序都可以用send()函数来向TCP连接的另一端发送数据。

函数原型:

1
int send(int s, const void *msg, size_t len, int flags);

参数:

  • s:指定发送端套接字描述符。

  • msg:指定要发送数据的缓冲区。

  • len:指明实际要发送的数据的字节数。

  • flags:一般设置为0即可

当调用该函数时,send()函数会先比较待发送数据的长度len和套接字s的发送缓冲的长度。

如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;

如果len小于或者等于s的发送缓冲区的长度,那么send()函数先检查协议是否 正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送s 的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send()函数就比较s的发送缓冲区的 剩余空间和len。

如果len大于剩余空间大小,send()函数就一直等待协议 把s的发送缓冲中的数据发送完。如果len小于剩余空间大小,send()函数就仅仅把buf中的数据 拷贝到s的发送缓冲区的剩余空间里。

如果send()函数拷贝数据成功,就返回实际copy的字节数,如果send()函数在拷贝数据时出现错误, 那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。

注意

send()函数把buf中的数据成功拷贝到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。

2.3.10. sendto函数

sendto()函数与send函数非常像,但是它会通过 struct sockaddr 指向的 to 结构体指定要发送给哪个远端主机,在to参数中需要指定远端主机的IP地址、端口号等,而tolen参数则是指定to 结构体的字节长度。

函数原型:

1
int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);

2.3.11. close函数

close()函数是用于关闭一个指定的套接字,在关闭套接字后,将无法使用对应的套接字描述符。

函数原型:

1
int close(int fd);

这个函数比较简单,当不需要使用某个套接字描述符时,就将其关闭即可,在UDP协议中,close会释放一个套接字描述符的资源; 而在TCP协议中,当调用close()函数后将发起“四次挥手”终止连接,当连接正式终止后,套接字描述符的资源才会被释放。

2.3.12. ioctlsocket函数

ioctlsocket()函数用于获取与设置套接字相关的操作参数。

函数原型:

1
int ioctlsocket( int s, long cmd, u_long *argp);

参数:

  1. s:指定要操作的套接字描述符。

  2. cmd:对套接字s的操作命令。

  • FIONBIO:命令用于允许或禁止套接字的非阻塞模式。在这个命令下, argp参数指向一个无符号长整型,如果该值为0则表示禁止非阻塞模式, 而如果该值非0则表示允许非阻塞模式。当创建一个套接字的时候,它就处于阻塞模式, 也就是说非阻塞模式被禁止,这种情况下所有的发送、接收函数都会是阻塞的, 直至发送、接收成功才得以继续运行;而如果是非阻塞模式下,所有的发送、接收函数都是不阻塞的, 如果发送不出去或者接收不到数据,将直接返回错误代码给用户, 这就需要用户对这些“意外”情况进行处理,保证代码的健壮性。

  • FIONREAD:FIONREAD命令确定套接字s自动读入的数据量,这些数据已经被接收, 但应用线程并未读取的,所以可以使用这个函数来获取这些数据的长度,在这个命令状态下, argp参数指向一个无符号长整型,用于保存函数的返回值(即未读数据的长度)。 如果套接字是SOCK_STREAM类型,则FIONREAD命令会返回recv()函数中所接收的所有数 据量,这通常与在套接字接收缓存队列中排队的数据总量相同;而如果套接字是 SOCK_DGRAM类型的,则FIONREAD命令将返回在套接字接收缓存队列中排队的 第一个数据包大小。

  • SIOCATMARK:确认是否所有的带外数据都已被读入。

  1. argp:指向cmd命令所带参数的指针。

其实这个函数,举个例子:

1
2
3
4
5
6
7
// 控制为阻塞模式。
u_long mode = 0;
ioctlsocket(s,FIONBIO,&mode);

// 控制为非阻塞模式。
u_long mode = 1;
ioctlsocket(s,FIONBIO,&mode);

2.3.13. getsockopt和setsockopt函数

这两个函数是用于获取/设置套接字的一些选项的。

1
2
3
4
5
int getsockopt(int sockfd, int level, int optname,
                void *optval, socklen_t *optlen);

int setsockopt(int sockfd, int level, int optname,
                const void *optval, socklen_t optlen);

参数level有多个常见的选项,如:

  • SOL_SOCKET:表示在Socket层。

  • IPPROTO_TCP:表示在TCP层。

  • IPPROTO_IP: 表示在IP层。

参数optname表示该层的具体选项名称,比如:

  • 对于SOL_SOCKET选项,可以是SO_REUSEADDR(允许重用本地地址和端口)、SO_SNDTIMEO(设置发送数据超时时间)、SO_SNDTIMEO(设置接收数据超时时间)、SO_RCVBUF(设置发送数据缓冲区大小)等等。

  • 对于IPPROTO_TCP选项,可以是TCP_NODELAY(不使用Nagle算法)、TCP_KEEPALIVE(设置TCP保活时间)等等。

  • 对于IPPROTO_IP选项,可以是IP_TTL(设置生存时间)、IP_TOS(设置服务类型)等等。

2.4. TCP通信示例

2.4.1. TCP客户端示例

我们本小节就通过socket API函数去实现一个TCP客户端,代码的步骤首先是与服务器建立连接,然后在客户端中输入一些数据并且将它发送到服务器,最后在数据发送完毕后就终止连接,由于TCP协议的模型是 客户端 <-> 服务器 ,因此我们在下一小节还会实现一个TCP服务器,两个进程间相互通信。

首先明确一下整个客户端的流程步骤:

  • 1.调用socket()函数创建一个套接字描述符。

  • 2.调用connect()函数连接到指定服务器中,端口号为服务器监听的端口号。

  • 3.调用write()函数发送数据。

  • 4.调用close()函数终止连接。

TCP客户端代码:

TCP客户端示例(base_linux/system_programing/tcp_client/sources/tcp_client.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define HOST "192.168.0.217"        // 根据你服务器的IP地址修改
#define PORT 6666                   // 根据你服务器进程绑定的端口号修改
#define BUFFER_SIZ (4 * 1024)           // 4k的数据区域


int main(void)
{
    int sockfd, ret;
    struct sockaddr_in server;
    char buffer[BUFFER_SIZ];        //用于保存输入的文本

    // 创建套接字描述符
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create an endpoint for communication fail!\n");
        exit(1);
    }

    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(PORT);
    server.sin_addr.s_addr = inet_addr(HOST);

    // 建立TCP连接
    if (connect(sockfd, (struct sockaddr *)&server, sizeof(struct sockaddr)) == -1) {
        printf("connect server fail...\n");
        close(sockfd);
        exit(1);
    }

    printf("connect server success...\n");

    while (1) {

        printf("please enter some text: ");
        fgets(buffer, BUFFER_SIZ, stdin);

        //输入了end,退出循环(程序)
        if(strncmp(buffer, "end", 3) == 0)
            break;

        write(sockfd, buffer, sizeof(buffer));
    }

    close(sockfd);
    exit(0);
}

2.4.2. TCP服务器示例

接着我们实现一个服务器代码,接受客户端的连接,并且将来自客户端的数据打印到终端中。

服务器的代码流程如下:

  1. 调用socket()函数创建一个套接字描述符。

  2. 调用bind()函数绑定监听的端口号。

  3. 调用listen()函数让服务器进入监听状态。

  4. 调用accept()函数处理来自客户端的连接请求。

  5. 调用read()函数接收客户端发送的数据。

  6. 调用close()函数终止连接。

服务器代码:

TCP服务器示例(base_linux/system_programing/tcp_server/sources/tcp_server.c文件)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <stdio.h>
#include <netdb.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>

#define MAX 10*1024
#define PORT 6666

// Driver function
int main()
{
    char buff[MAX];
    int n;
    int sockfd, connfd, len;
    struct sockaddr_in server, client;

    // socket create and verification
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        printf("socket creation failed...\n");
        exit(0);
    }

    printf("socket successfully created..\n");
    bzero(&server, sizeof(server));

    // assign IP, PORT
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = htonl(INADDR_ANY);
    server.sin_port = htons(PORT);

    // binding newly created socket to given IP and verification
    if ((bind(sockfd, (struct sockaddr*)&server, sizeof(server))) != 0) {
        printf("socket bind failed...\n");
        exit(0);
    }

    printf("socket successfully binded..\n");

    // now server is ready to listen and verification
    if ((listen(sockfd, 5)) != 0) {
        printf("Listen failed...\n");
        exit(0);
    }

    printf("server listening...\n");

    len = sizeof(client);

    // accept the data packet from client and verification
    connfd = accept(sockfd, (struct sockaddr*)&client, &len);
    if (connfd < 0) {
        printf("server acccept failed...\n");
        exit(0);
    }

    printf("server acccept the client...\n");

    // infinite loop for chat
    while(1) {
        bzero(buff, MAX);

        // read the messtruct sockaddrge from client and copy it in buffer
        if (read(connfd, buff, sizeof(buff)) <= 0) {
            printf("client close...\n");
            close(connfd);
            break;
        }

        // print buffer which contains the client contents
        printf("from client: %s\n", buff);

        // if msg contains "Exit" then server exit and chat ended.
        if (strncmp("exit", buff, 4) == 0) {
            printf("server exit...\n");
            close(connfd);
            break;
        }
    }

    // After chatting close the socket
    close(sockfd);
    exit(0);
}

2.4.3. 实验操作

本示例代码在system_programing/tcp_client和tcp_server目录下, 分别编译并且运行即可,现象如下:

服务器端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 以下操作在 system_programing/tcp_server代码目录进行
make

# 运行
./build/tcp_server_demo

# 信息输出如下
socket successfully created..
socket successfully binded..
server listening...

客户端

打开一个 新终端 ,切换至tcp_client目录编译并运行:

1
2
3
4
5
6
7
8
9
# 以下操作在 system_programing/tcp_client代码目录进行
make

# 运行
./build/tcp_client_demo

# 信息输出如下
connect server success...
please enter some text:

在客户端的终端输入一些内容,然后再查看服务器端的终端是否有信息打印,最后在服务器端的终端输入exit结束。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 在客户端的终端输入
connect server success...
please enter some text: 你好
please enter some text: test
please enter some text: hello world
please enter some text: exit

# 在服务器端的终端输出信息如下
socket successfully created..
socket successfully binded..
server listening...
server acccept the client...
from client: 你好

from client: test

from client: hello world

client close...

如果成功连接后,Ctrl+C退出了服务器端程序,再次运行提示socket bind failed——这是因为TCP端口在强制退出后会进入TIME_WAIT状态, 导致新进程无法绑定同一个端口,此时等待一会重新运行即可。