TCP server
在cs模型中,server只需要等待client的连接,因此server包括下面四个步骤:
- 调用socket(),初始化一个socket实例,
- 调用bind(),将socket绑定到网卡上,
- 调用listen(),等待客户端的连接,
- 与客户端三次握手后,调用accept()。
server的代码如下:
//server.cpp
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <errno.h>
#include <sys/socket.h>
#include <iostream>
#include <cstring>
int main(){
const int port = 9007;
const std::string ip= "127.0.0.1";
//初始化socket
int server_sock = socket(AF_INET, SOCK_STREAM, 0);
if(server_sock < 0){
std::cout << "create socket failed:" << strerror(errno) << std::endl;
return 0;
}
//绑定地址,sockaddr_in是sockaddr的升级,sockaddr已经弃用
struct sockaddr_in server;
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
//将主机字节序转换为网络字节序
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(server);
//绑定时,由于bind函数设计较早,因此还是需要将sockarr_in转换成sockaddr
if(bind(server_sock, (struct sockaddr*) &server, len)<0){
std::cout << "bind socket failed:" << strerror(errno) << std::endl;
return 0;
}
//调用listen,等待客户端连接,listen第二个参数为全连接队列的大小
listen(server_sock, 5);
char buf[1024];
while(1){
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
bzero(&client,sizeof(client));
std::cout << "------------waiting for connect-------------" << std::endl;
int client_sock = accept(server_sock, (struct sockaddr*) &client, &client_len);
if(client_sock < 0){
std::cout << "accept socket failed:" << strerror(errno) << std::endl;
continue;
}
int client_port = ntohs(client.sin_port);
char client_ip[1024];
const char *ptr = inet_ntop(AF_INET,&client.sin_addr, client_ip, sizeof(client_ip));
if(!ptr){
std::cout << "transform client address failed:" << strerror(errno) << std::endl;
}
int nbytes;
while(nbytes = read(client_sock, buf, sizeof(buf)-1)>0){
std::cout << "receive msg:" << buf << std::endl;
}
close(client_sock);
}
close(server_sock);
return 0;
}
需要注意的是,server调用accept后,创建了一个新的socket与客户端通讯,而不是直接用server_sock,否则每个server只能连接一个client。
TCP client
client需要主动向server发起请求,需要的操作如下:
- 调用socket(),初始化一个socket实例,
- 调用connect(),发起三次握手,建立连接,
- 向server发送数据。
//client.cpp
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <errno.h>
#include <sys/socket.h>
#include <iostream>
#include <cstring>
int main(){
const int port = 9007;
const std::string ip= "127.0.0.1";
int client_sock = socket(AF_INET, SOCK_STREAM, 0);
if(client_sock < 0){
std::cout << "create socket failed:" << strerror(errno) << std::endl;
return 0;
}
struct sockaddr_in server;
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(server);
if(connect(client_sock, (struct sockaddr*) &server, len)<0){
std::cout << "connect socket failed:" << strerror(errno) << std::endl;
return 0;
}
std::string msg;
while(std::cin >> msg){
if(msg == "EOF")break;
write(client_sock, msg.c_str(), sizeof(msg));
}
close(client_sock);
return 0;
}
c/s模型通信验证
将上面的server.cpp编译为server,client.cpp编译为client,运行程序建立通讯,并使用wireshark抓包。
在上面的过程中,client已经调用了connect()
函数,wireshark中捕获到了相应的三次握手操作:
端口48154是操作系统自动为client分配的端口,三次握手与我们在TCP(二)中描述的一致。此时可以从client发送数据给server:
此时wireshark的抓包情况如下:
client发送了32字节的数据给server,这32字节的数据包括20字节的TCP固定首部和12字节的“hello,world!",server确认ack为32+1 = 33,这序号从0开始是因为wireshark为了方便分析,将序列号都转换为了相对序列号。
最后我们断开连接,看看四次挥手的情况。
这里四次挥手变成了三次,是因为在close_wait状态server没有需要发送的数据,因此直接进入了last_ack状态。
这里我们再通过netstat,可以看到client的time_wait状态。
localhost的端口号位48452是因为之前忘记截图了,重新运行client进行四次挥手,因此操作系统重新分配了端口号,这也说明client的端口号是随机分配的。
netstat是Linux下非常强大的网络工具,通过netstat可以看到tcp连接各状态的变化,但由于变化很快不好截图,感兴趣的小伙伴可以自己尝试一下。
总结
本文通过运行tcp通信例程,验证了前面介绍的内容,同时介绍了几个linux下强大的网络分析工具,这些工具还有很多,有机会接触我们继续介绍,下篇文章将对tcp连接的疑难杂症进行介绍和试验。