本文整理下tcp连接的基本使用,基本的函数使用,最后整体上通过一个例子来测试流程。

例子的基本功能:

服务端与客户端连接

常用函数

socket: 创建socket句柄

int socket(int family,int type,int protocol);

bind: 绑定本地端口

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

为套接口分配一个本地IP和协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合;如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。

listen:监听客户端连接

int listen(int sockfd,int backlog);

listen函数仅被TCP服务器调用,它的作用是将用sock创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。

第二个参数规定了内核为此套接口排队的最大连接个数。由于listen函数第二个参数的原因,内核要维护两个队列:以完成连接队列和未完成连接队列。未完成队列中存放的是TCP连接的三路握手未完成的连接,accept函数是从已连接队列中取连接返回给进程;当以连接队列为空时,进程将进入睡眠状态。

accept:服务端连接一路接入的客户端

int accept(int listenfd, struct sockaddr *client, socklen_t * addrlen);

accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。

connect: 客户端去连接服务端

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

当用socket建立了套接口后,可以调用connect为这个套接字指明远程端的地址;如果是字节流套接口,connect就使用三次握手建立一个连接;如果是数据报套接口,connect仅指明远程端地址,而不向它发送任何数据

send和recv函数

TCP套接字提供了send()和recv()函数,用来发送和接收操作。

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

select函数模型

使用select函数去查询使用的socket的状态。比如,是否有客户端连接,是否有客户端发送数据。这样就可以针对有数据的socket进行操作。

int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds, struct timeval *timeout);  

fd_set状态集合

下面是4个可以对集合进行操作的宏:

void FD_CLR(int fd, fd_set *set);   // 清除出集合
int  FD_ISSET(int fd, fd_set *set); // 判断是否在集合中
void FD_SET(int fd, fd_set *set);   // 添加进集合中
void FD_ZERO(fd_set *set);          // 将集合清零

服务端的select模型例子

#include <stdio.h>  
#include <stdbool.h>
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <sys/types.h>  
#include <sys/socket.h>  
#include <netinet/in.h>  
#include <arpa/inet.h>  
#include <signal.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/select.h>

void sigint_handle(int sig);
int socket_init();
void socket_deinit(int fd);
void socket_accept(int fd);

unsigned char quit = 0;

#define FD_MAX_CNT 100
int client[FD_MAX_CNT] = {0};

int max_fd(int fd) {
    int maxfd = fd;
    for(int i=0; i<FD_MAX_CNT; ++i) {
        if( client[i] > maxfd ) {
            maxfd = client[i];
        }
    }

    return maxfd;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        printf("Usage: %s <Listen Port>\n", argv[0]);
        return -1;
    }

    signal(SIGINT, sigint_handle);

    for (int i = 0; i < FD_MAX_CNT; i++)
        client[i] = -1;  

    int listenfd = socket_init(atoi(argv[1]));
    if (listenfd < 0) {
        exit(-1);
    }

    socket_accept(listenfd);

    socket_deinit(listenfd);
    return 0;
}

void sigint_handle(int sig) {
    quit = 1;
}

int socket_init(int port) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        printf("socket create failed.\n");
        goto _error;
    }

    // non-block
    int x = fcntl(fd, F_GETFL,0);  
    fcntl(fd, F_SETFL, x | O_NONBLOCK);  

    //reuse address
    int opt = SO_REUSEADDR;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); 

    //bind
    struct sockaddr_in addr;  
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        printf("socket bind failed.\n");
        goto _error;
    }

    //listen
    if(listen(fd, 10) < 0) {
        printf("socket listen failed.\n");
        goto _error;
    }  

    return fd;
_error:
    socket_deinit(fd);
    return -1;
}

void socket_accept(int fd) {
    struct sockaddr_in client_addr;
    socklen_t addrlen;

    struct timeval timeout = {3, 0};
    fd_set read_flags, write_flags;
    char buffer[8196] = {0};

    while (!quit) {
        FD_ZERO(&read_flags); 
        FD_ZERO(&write_flags); 

        FD_SET(fd, &read_flags);

        // set client ids
        for(int i=0; i<FD_MAX_CNT; ++i) {
            if(client[i] > 0) {
                FD_SET(client[i], &read_flags);
            }
        }
        
        int maxfd = max_fd(fd);
        int ret = select(maxfd+1, &read_flags, &write_flags, NULL, &timeout);
        if (ret < 0) {
            break;
        } else if (ret == 0) {
            continue;
        } else {
            // check read
            for(int i=0; i<FD_MAX_CNT; ++i) {
                int clientId = client[i];
                if(clientId > 0) {
                    if (FD_ISSET(clientId, &read_flags)) {
                        char buf[1024] = {0};
                        int len = recv(clientId, buf, sizeof(buf), 0);
                        if (len > 0) {
                            buf[len] = '\0';
                            send(clientId, buf, strlen(buf), 0);
                            printf("Recv from fd: %d, data: %s\n", clientId, buf);
                        } else if (len == 0) {
                            printf("Disconnect from fd: %d\n", clientId);
                            // remove client id
                            for(int i=0; i<FD_MAX_CNT; ++i) {
                                if( client[i] == clientId ) {
                                    client[i] = -1;
                                    break;
                                }
                            }
                        } else {

                        }
                    }
                }
            }

            // check new client
            int clientfd = accept(fd, (struct sockaddr*)&client_addr, &addrlen);
            if (clientfd < 0) {
                // no new client
            } else {
                printf("Accept: %s:%d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port));
                send(clientfd, "Welcome", 8, 0);

                // add client id
                for(int i=0; i<FD_MAX_CNT; ++i) {
                    if( client[i] < 0 ) {
                        client[i] = clientfd;
                        break;
                    }
                }
            }            
        }
    }
}

void socket_deinit(int fd) {
    if (fd >= 0) close(fd);
}

客户端程序例子

#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>  
#include <fcntl.h>
#include <strings.h>

int socket_init();
void socket_deinit();
void socket_connect(int fd, char* ip, int port);

int main(int argc, char* argv[]) {
    if (argc != 3) {
        printf("Usage: %s <Server IP> <Server Port>\n", argv[0]);
        return -1;
    }

    int sockfd = socket_init();
    socket_connect(sockfd, argv[1], atoi(argv[2]));
    printf("Connected to %s\n", argv[1]);

    while(true) {
         // recv from server
        char buf[1024] = {0};
        int len = recv(sockfd, buf, sizeof(buf), 0);
        if (len >= 0) {
            buf[len] = '\0';
        } else {
            printf("Client break%s\n");
            break;
        }
        printf("Recv: %s\n", buf);

        // send to server
        char sendbuf[1024] = {0};
        scanf("%s", sendbuf);
        int ret = send(sockfd, sendbuf, strlen(sendbuf), 0);				
        if(ret < 0) {
            printf("Client break%s\n");
            break;
        }
        printf("Send: %s\n", sendbuf);

        sleep(1);
    }

    socket_deinit(sockfd);
    return 0;
}

void socket_connect(int fd, char* ip, int port) {
    struct sockaddr_in addr;
    socklen_t addrlen = sizeof(addr);

    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip);

    if (connect(fd, (struct sockaddr*)&addr, addrlen) < 0) {
        perror("socket connect failed");
        exit(-1);
    }
}

int socket_init() {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket create failed.");
        exit(-1);
    }

    return fd;
}

void socket_deinit(int fd) {
    if (fd >= 0) close(fd);
}

关于定义协议包的注意

实际应用中,由于tcp传输是连续流式的,需要定义可识别的协议结构,需要增加一个缓冲区,比如可以定义一个TestProtocolHead用来做包识别。

// TCP head protocol
struct TestProtocolHead
{
  TestProtocolHead()
  {
    memset(this, 0, sizeof(TestProtocolHead));
  }
  
  short magic;
  unsigned int cmd;
  unsigned int body_length;
};

const short TEST_HEAD_MAGIC = 0x1111;
- (void)recvMsg: (NSData*)data
{
  std::string strMsg = std::string((const char*)[data bytes], [data length]);
  _recv_buf.append( strMsg );
  
  while ( _recv_buf.size() >= sizeof(TestProtocolHead) ) {
    int invalidCharCount = 0;
    for (unsigned i = 0; i < _recv_buf.size() - 1; ++i)
    {
      short magicNum = 0;
      memcpy(&magicNum, _recv_buf.data() + i, sizeof(short));
      magicNum = base::NetToHost16(magicNum);
      if (magicNum == TEST_HEAD_MAGIC)
      {
        break;
      }
      invalidCharCount++;
    }
    if (invalidCharCount)
    {
      _recv_buf = _recv_buf.substr(invalidCharCount);
    }
    
    TestProtocolHead head;
    memcpy(&head, _recv_buf.c_str(), sizeof(TestProtocolHead));
    head.magic = base::NetToHost16(head.magic);
    head.cmd = base::NetToHost32(head.cmd);
    head.body_length = base::NetToHost32(head.body_length);
    
    if (_recv_buf.size() >= sizeof(head) + head.body_length ) {
      std::string strBody = _recv_buf.substr(sizeof(TestProtocolHead), head.body_length);
      
      dispatch_async(dispatch_get_main_queue(), ^{
        // 完整包发送给业务处理
      });
      
      _recv_buf = _recv_buf.substr(sizeof(head) + head.body_length);
    }
  }
}

- (BOOL)sendMsg:(int)cmd msg:(NSString*)msg
{
  std::string msgBuf = [msg UTF8String];
  
  TestProtocolHead head;
  head.magic = base::HostToNet16(TEST_HEAD_MAGIC);
  head.cmd = base::HostToNet32(cmd);
  head.body_length = base::HostToNet32(msgBuf.size());
  
  std::string buf;
  buf.assign((char*)&head, sizeof(head));
  buf.append(msgBuf);

  if ( _clientfd != -1 ) {
    ::send(_clientfd, buf.data(), buf.size(), 0);
    return YES;
  }
  
  return NO;
}

关于服务端端口复用

tcp服务端使用一个端口,但是accept函数接入客户端时,用户的ip + port对都是唯一的。

测试例子

server:

$ ./tcp_server 1935
Accept: 127.0.0.1:57600
Accept: 127.0.0.1:57601
Recv from fd: 4, data: 111
Recv from fd: 5, data: 222
Recv from fd: 4, data: 333
Recv from fd: 5, data: 444
Disconnect from fd: 5
Disconnect from fd: 4
client 1:

$ ./tcp_client 127.0.0.1 1935
Connected to 127.0.0.1
Recv: Welcome
111
Send: 111
Recv: 111
333
Send: 333
Recv: 333
client 2:

./tcp_client 127.0.0.1 1935
Connected to 127.0.0.1
Recv: Welcome
222
Send: 222
Recv: 222
444
Send: 444
Recv: 444

参考