本文整理下tcp连接的基本使用,基本的函数使用,最后整体上通过一个例子来测试流程。
例子的基本功能:
- 服务端:
- 接入”Welcome”
- 支持并发
- 收到客户端信息,原样返回给客户端
- 客户端:
- 支持交互式的发送数据给服务端
服务端与客户端连接
常用函数
socket: 创建socket句柄
int socket(int family,int type,int protocol);
- 第一个参数指明了协议簇,目前支持5种协议簇,最常用的有AF_INET(IPv4协议)和AF_INET6(IPv6协议);
- 第二个参数指明套接口类型,有三种类型可选:SOCK_STREAM(字节流套接口)、SOCK_DGRAM(数据报套接口)和SOCK_RAW(原始套接口);
- 如果套接口类型不是原始套接口,那么第三个参数就为0
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服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。
- 第一个参数是socket函数返回的套接口描述字;
- 第二个和第三个参数分别是一个指向连接方的套接口地址结构和该地址结构的长度;该函数返回的是一个全新的套接口描述字;如果对客户段的信息不感兴趣,可以将第二和第三个参数置为空。
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);
- maxfdp:是一个整数值,是指集合中所有文件描述符(socket)的范围,即所有文件描述符的最大值加1。
- readfds,writefds,errorfds都是fd_set类型的指针,是个socket的数组指针。
- readfds:进行可读操作查询的文件描述符的集合,不关心读操作,则可传入NULL
- writefds:进行可写操作查询的文件描述符的集合,不关心写操作,则可传入NULL。
- errorfds:进行文件异常错误的查询的文件描述符,也可以传入NULL
- timeout:
- 若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
- 若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行;
- timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回
- 返回值:如果有错误,则返回值为-1,如果超时返回,则返回值为0,如果,文件描述符里面有变化,则返回值大于0
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
参考
- https://blog.csdn.net/codeheng/article/details/44814129
- https://www.jianshu.com/p/86d7ed4b740e
- https://blog.csdn.net/yueguanghaidao/article/details/7035248
- https://linux.die.net/man/2/select
- http://www.cs.tau.ac.il/~eddiea/samples/IOMultiplexing/
- https://segmentfault.com/q/1010000003101541
- http://www.voidcn.com/article/p-qhgftyls-gz.html
- https://blog.csdn.net/u012814360/article/details/39402685
- https://blog.csdn.net/sprintfwater/article/details/9234399
- https://stackoverflow.com/questions/17817874/detecting-whenever-socket-disconnected-using-select