通信流程
一、建立连接
- 服务器调⽤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
int socket(int domain, int type, int protocol);
功能:
socket()打开⼀个⽹络通讯端口,如果成功的话,就像open()⼀样返回⼀个⽂件描述符,应⽤程序可以像读写⽂件⼀样⽤read/write在⽹络上收发数据,如果socket()调⽤出错则返回-1。
参数:
- 对于IPv4,family参数指定为AF_INET;
- 对于TCP协议,type参数指定为SOCK_STREAM,表⽰⾯向流的传输协议;如果是UDP协议,则type参数指定为SOCK_DGRAM,表⽰⾯向数据报的传输协议;
- protocol参数的介绍从略,指定为0即可。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:
服务器程序所监听的⽹络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调⽤bind绑定⼀个固定的⽹络地址和端口号。
bind()成功返回0,失败返回-1。
参数:
struct sockaddr *是⼀个通⽤指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,⽽它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。
int listen(int sockfd, int backlog);
功能:
声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调⽤的accept()返回并接受这个连接,如果有⼤量的客户端发起连接⽽服务器来不及处理,尚未accept 的客户端就处于连接等待状态。
参数:
- addr是⼀个传入传出参数,accept()返回时传出客户端的地址和端口号若给addr 参数传NULL,表示不关⼼客户端的地址;
- addrlen是⼀个传⼊传出参数,传⼊的是调⽤者提供的缓冲区addr 的长度,以避免缓冲区溢出问题;传出的是客户端地址结构体的实际长度(有
可能没有占满调⽤者提供的缓冲区)。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端需要调⽤connect()连接服务器,connect和bind的参数形式⼀致,区别在于bind的参数是⾃⼰的地址,⽽connect的参数是对⽅的地址。connect()成功返回0,出错返回-1。
通信代码
服务器端:
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
static void usage(const char *proc)
{
printf("%s [local_ip] [local_port]\n", proc);
}
//success return socket,failed return -1
int startup(const char *_ip, int _port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
perror("bind");
exit(3);
}
if(listen(sock, 5) < 0)
{
perror("listen");
exit(4);
}
return sock;
}
void *handlerRequest(void *arg)
{
int new_fd = (int)arg;
while(1)
{
char buf[1024];
ssize_t s = read(new_fd, buf, sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("client: %s\n", buf);
write(new_fd, buf, strlen(buf));
}
else{
printf("read done...break\n");
break;
}
}
}
// ./tcp_server 127.0.0.1 8080
int main(int argc, char *argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int listen_sock = startup(argv[1], atoi(argv[2]));
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock, (struct sockaddr*)&client, &len);
if(new_sock < 0)
{
perror("accept");
continue;
}
//version1 单进程版本
//while (1)
//{
// char buf[1024];
// ssize_t s = read(new_sock, buf, sizeof(buf)-1);
// if(s > 0)
// {
// buf[s] = 0;
// printf("client: %s\n", buf);
// write(new_sock, buf, strlen(buf));
// }
// else
// {
// printf("read done...break\n");
// break;
// }
//}
//version2 多线程版本
//printf("get a new client, %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
//pthread_t id;
//pthread_create(&id, NULL, handlerRequest, (void*)new_sock);
//pthread_detach(id);
//vision3 多进程版本
printf("get a new client, %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
pid_t id = fork();
if(id < 0)
{
perror("fork");
continue;
//close(new_sock);
}
else if(id == 0)
{
//child
close(listen_sock);
if(fork() > 0)
{
exit(0);
}
while (1)
{
char buf[1024];
ssize_t s = read(new_sock, buf, sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("client: %s\n", buf);
//write(new_sock, buf, strlen(buf));
}
else if(s == 0)
{
printf("read done...\n");
break;
}
else{
printf("client gone...\n");
break;
}
printf("Please Enter: ");
fflush(stdout);
ssize_t _s = read(0, buf, sizeof(buf)-1);
if(_s > 0)
{
buf[_s-1] = 0;
write(new_sock, buf, strlen(buf));
}
}
close(new_sock);
exit(5);
}
else{//father
close(new_sock);
waitpid(id, NULL, 0);
}
close(new_sock);
}
return 0;
}
客户端:
static void usage(const char *proc)
{
printf("%s [local_ip] [local_port]\n", proc);
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
perror("socket");
return 2;
}
struct sockaddr_in remote; //远端主机
remote.sin_family = AF_INET;
remote.sin_port = htons(atoi(argv[2]));
remote.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock, (struct sockaddr*)&remote, sizeof(remote)) < 0)
{
perror("connect");
return 3;
}
while (1)
{
char buf[1024];
printf("Please Enter: ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if(s > 0)
{
buf[s-1] = 0;
write(sock, buf, strlen(buf));
ssize_t _s = read(sock, buf, sizeof(buf)-1);
if(_s > 0)
{
buf[_s] = 0;
printf("server: %s\n", buf);
}
}
}
}
运行结果:
从运行结果看来目前我们的程序好像没什么毛病了,但是我们来做个实验:启动server和启动client,然后⽤Ctrl-C使server终⽌,这时马上再运行server:
bind报错了!说地址被占用了,可是server不是刚刚被我们终止了吗?
这是因为,虽然server的应⽤程序终⽌了,但TCP协议层的连接并没有
完全断开,因此不能再次监听同样的server端口。
我们把client也终止掉,client终⽌时⾃动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状 态。
TCP协议规定,主动关闭连接的⼀⽅要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终⽌了server,所以server是主动关闭连接的⼀⽅,TIME_WAIT期间仍然不能再次监听同样的server端口。MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Linux上⼀般经过半分钟后 就可以再次启动server了。
但是在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为,TCP连接没有完全断开 指的是connfd(127.0.0.1:8000)没有完全断开,⽽我们重新监听的是listenfd(0.0.0.0:8000), 虽然是占⽤同⼀个端口,但IP 地址不同,connfd 对应的是与某个客户端通讯的⼀个具体的IP 地址, ⽽listenfd对应的是wildcard address。解决这个问题的⽅法是使⽤setsockopt()设置socket描述符的 选项SO_REUSEADDR为1,表⽰允许创建端口号相同但IP地址不同的多个socket描述符。 在server代码的socket()和bind()调⽤之间插⼊如下代码:
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));