Skip to content

基于TCP的socket编程

TCP协议通讯流程

下图是基于TCP协议的客户端/服务器程序的一般流程:

服务器调用socket()bind()listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

数据传输的过程

建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

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

在学习socket API时要注意应用程序和TCP协议层是如何交互的: 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段

最简单的C/S实例

下面通过最简单的客户端/服务器程序的实例来学习socket API。

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

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 6666

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

    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
    
    listen(listenfd, 20);

    printf("Accepting connections ...\n");
    while (1) {
        cliaddr_len = sizeof(cliaddr);
        connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &cliaddr_len);
        
        n = read(connfd, buf, MAXLINE);
        printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));

        for (i = 0; i < n; i++)
            buf[i] = toupper(buf[i]);
        write(connfd, buf, n);
        
        close(connfd);
    }
    
    return 0;
}

client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印。

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 6666

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

    if (argc != 2) {
        fputs("usage: ./client message\n", stderr);
        return -1;
    }

    str = argv[1];
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);
    connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
    
    write(sockfd, str, strlen(str));
    
    n = read(sockfd, buf, MAXLINE);
    printf("Response from server:\n");
    write(STDOUT_FILENO, buf, n);
    
    close(sockfd);

    return 0;
}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

客户端和服务器启动后可以使用netstat命令查看链接情况:

shell
netstat -apn|grep 6666

出错处理封装函数

上面的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。

函数封装思想

结合man-page和errno进行封装。在封装的时候起名可以把第一个函数名的字母大写,如socket可以封装成Socket,这样可以按shift+k进行搜索,shift+k搜索函数说明的时候不区分大小写,使用man page也可以查看,man page对大小写不区分。

acceptread这样的能够引起阻塞的函数,若被信号打断,由于信号的优先级较高,会优先处理信号;信号处理完成后,会使accept或者read解除阻塞,然后返回,此时返回值为 -1,设置errno=EINTR

errno=ECONNABORTED表示连接被打断,异常。

errno宏

/usr/include/asm-generic/errno.h文件中包含了errno所有的宏和对应的错误描述信息。

粘包的概念

粘包: 多次数据发送,首尾相连,接收端接收的时候不能正确区分第一次发送多少,第二次发送多少。

粘包问题分析和解决??

  • 方案1: 包头+数据
    • 如4位的数据长度+数据 -----------> 00101234567890
    • 其中0010表示数据长度, 1234567890表示10个字节长度的数据
    • 另外,发送端和接收端可以协商更为复杂的报文结构,这个报文结构就相当于双方约定的一个协议
  • 方案2: 添加结尾标记
    • 如结尾最后一个字符为\n $等
  • 方案3: 数据包定长
    • 如发送方和接收方约定,每次只发送128个字节的内容,接收方接收定长128个字节就可以了

为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块wrap.c

wrap.c代码解读和分析

要求能看懂代码。会使用即可

wrap.c
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>

void perr_exit(const char *s){
    perror(s);
    return -1;
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr){
    int n;

again:
    if ((n = accept(fd, sa, salenptr)) < 0) {
        if ((errno == ECONNABORTED) || (errno == EINTR))
            goto again;
        else
            perr_exit("accept error");
    }
    return n;
}

int Bind(int fd, const struct sockaddr *sa, socklen_t salen){
    int n;

    if ((n = bind(fd, sa, salen)) < 0)
        perr_exit("bind error");
    return n;
}

int Connect(int fd, const struct sockaddr *sa, socklen_t salen){
    int n;

    if ((n = connect(fd, sa, salen)) < 0)
        perr_exit("connect error");
    return n;
}

int Listen(int fd, int backlog){
    int n;

    if ((n = listen(fd, backlog)) < 0)
        perr_exit("listen error");
    return n;
}

int Socket(int family, int type, int protocol){
    int n;

    if ((n = socket(family, type, protocol)) < 0)
        perr_exit("socket error");
    return n;
}

ssize_t Read(int fd, void *ptr, size_t nbytes){
    ssize_t n;
again:
    if ((n = read(fd, ptr, nbytes)) == -1) {
        if (errno == EINTR)
            goto again;
        else
            return -1;
    }
    return n;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes){
    ssize_t n;
again:
    if ((n = write(fd, ptr, nbytes)) == -1) {
        if (errno == EINTR)
            goto again;
        else
            return -1;
    }
    return n;
}

int Close(int fd){
    int n;

    if ((n = close(fd)) == -1)
        perr_exit("close error");
    return n;
}

ssize_t Readn(int fd, void *vptr, size_t n){
    size_t nleft;
    ssize_t nread;
    char *ptr;
    ptr = vptr;
    nleft = n;

    while (nleft > 0) {
        if ((nread = read(fd, ptr, nleft)) < 0) {
            if (errno == EINTR)
                nread = 0;
            else
                return -1;
        } else if (nread == 0)
            break;
        nleft -= nread;
        ptr += nread;
    }
    return n - nleft;
}

ssize_t Writen(int fd, const void *vptr, size_t n){
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;
    ptr = vptr;
    nleft = n;

    while (nleft > 0) {
        if ((nwritten = write(fd, ptr, nleft)) <= 0) {
            if (nwritten < 0 && errno == EINTR)
                nwritten = 0;
            else
                return -1;
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}

static ssize_t my_read(int fd, char *ptr){
    static int read_cnt;
    static char *read_ptr;
    static char read_buf[100];

    if (read_cnt <= 0) {
    again:
        if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
            if (errno == EINTR)
                goto again;
            return -1;
        } else if (read_cnt == 0)
            return 0;
        read_ptr = read_buf;
    }
    read_cnt--;
    *ptr = *read_ptr++;
    return 1;
}

ssize_t Readline(int fd, void *vptr, size_t maxlen){
    ssize_t n, rc;
    char c, *ptr;
    ptr = vptr;

    for (n = 1; n < maxlen; n++) {
        if ((rc = my_read(fd, &c)) == 1) {
            *ptr++ = c;
            if (c == '\n')
                break;
        } else if (rc == 0) {
            *ptr = 0;
            return n - 1;
        } else
            return -1;
    }
    *ptr = 0;
    return n;
}
wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_

void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);

#endif

基于TCP的字母大小写转换

自己分别编写服务端和客户端程序,实现两个程序之间的通信。服务端将客户端发来的字母转换为大写字母返回给客户端

server.c
#include <arpa/inet.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    // 1.创建socket通信文件描述符
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sfd < 1) {
        perror("socket error");
        return -1;
    }

    // 2.绑定IP地址及端口
    struct sockaddr_in myaddr;
    myaddr.sin_family = AF_INET;
    myaddr.sin_port = htons(8888);
    inet_pton(AF_INET, "127.0.0.1", &myaddr.sin_addr.s_addr);
    int retbind = bind(sfd, (struct sockaddr *) &myaddr, sizeof(myaddr));
    if (retbind < 0) {
        perror("bind error");
        return -1;
    }

    // 3.监听socket通信文件描述符有没有客户端进行连接
    listen(sfd, 20);
    struct sockaddr_in othaddr;
    bzero(&othaddr, sizeof(othaddr));
    int othlen = sizeof(othaddr);
    
    // 4.接收客户端的连接,并且生成通信文件描述符
    int afd = accept(sfd, (struct sockaddr *) &othaddr, &othlen);
    if (afd < 0) {
        perror("accept error");
        return -1;
    }

    // 5.对接收到或者要发送的数据进行处理
    char buf[1024];
    int ret = 1;
    int i = 0;
    bzero(&buf, sizeof(buf));
    while (1) {
        ret = read(afd, buf, sizeof(buf));
        if (ret <= 0) {
            printf("read error or client closed. ret=[%d]\n", ret);
            break;
        }

        printf("ret=[%d],buf=[%s]\n", ret, buf);
        for (i = 0; i < ret; i++) {
            buf[i] = toupper(buf[i]);
        }

        write(afd, buf, ret);
    }

    // 6.通信完成后关闭使用过的文件描述符
    close(sfd);
    close(afd);

    return 0;
}
client.c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    // 1.创建socket通信文件描述符
    int cfd = socket(AF_INET, SOCK_STREAM, 0);
    if (cfd < 0) {
        perror("socket error");
        return -1;
    }

    // 2.连接服务端
    struct sockaddr_in clientaddr;
    clientaddr.sin_family = AF_INET;
    clientaddr.sin_port = htons(8888);
    inet_pton(AF_INET, "127.0.0.1", &clientaddr.sin_addr);
    //int len = sizeof(clientaddr);
    int cret = connect(cfd, (struct sockaddr *) &clientaddr, sizeof(clientaddr));
    if (cret < 0) {
        perror("socket error");
        return -1;
    }

    // 3.对接收到或要发送到数据进行处理
    char buf[1024];
    int ret = 1;
    while (1) {
        bzero(&buf, sizeof(buf));
        ret = read(STDIN_FILENO, buf, sizeof(buf));

        write(cfd, buf, ret);

        bzero(&buf, sizeof(buf));
        ret = read(cfd, buf, sizeof(buf));
        if (ret <= 0) {
            printf("read error or server closed. ret=[%d]\n", ret);
            break;
        }

        printf("ret=[%d],buf=[%s]\n", ret, buf);
    }

    // 4.关闭使用过的文件描述符
    close(cfd);

    return 0;
}

让服务端无视TIME_WAIT

使用setsockopt函数在服务端bind之前修改socket的属性,

函数原型

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

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

函数参数

  • sockfd:socket返回的文件描述符
  • level:固定设置为SOL_SOCKET
  • optname:要修改哪个属性,可以使用man 7 socket查看支持修改的属性,这里固定设置为SO_REUSEADDR
  • optval:要修改成什么样,1为真,0为假。这里设置为真
  • optlenoptval的大小,这里直接sizeof(int)即可

返回值

  • 成功:0
  • 失败:-1,并设置errno

所以在bind之前加入这几句代码即可

cpp
int optval = 1;
// int ret = setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
int ret = setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int));
if (ret == -1){
    perror("setsockopt");
    return -1;
}