网络编程套接字---tcp简单通信程序详解
tcp简单通信程序详解
本篇博客的目的是用socket套接字及接口实现一个简单的tcp聊天程序。
实现步骤
服务端:
- 创建套接字listen_sock
- 绑定地址信息(IP地址,端口号)
- 监听套接字listen_sock
- 与服务端建立链接
- 接收客户端发来的数据
- 向客户端发送数据
- 关闭套接字
客户端:
8. 创建套接字sockfd
9. 绑定地址信息(内部完成,不手动绑定)
10. 申请与客户端建立连接
11. 建立连接成功
12. 向服务端发送数据
13. 接收服务端发来的数据
14. 关闭套接字
服务端:
1.创建套接字socket
//1.创建监听套接字listen_sock,参数依次为版本IPv4,流式套接字,默认tcp
int listen_sock = socket(AF_INET,SOCK_STREAM,0);
if (listen_sock < 0)
{
perror("create socket failed!");
return -1;
}
2.绑定地址信息
//2.为服务器绑定地址信息,以后凡是向服务器发起连接请求,都会首先让listen_sock处理。
struct sockaddr_in server_addr; //sockaddr_in仅用于IPv4
socklen_t size = sizeof(server_addr);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));//先将命令行第二个参数(端口号)转为整形再转为网络字节序。
server_addr.sin_addr.s_addr = inet_addr(argv[1]);//将命令行第一个参数(ip地址)转为网络字节序。
int ret = bind(listen_sock,(struct sockaddr*)&server_addr,size);
这里创建一个地址信息块,存储了服务端的IP地址和端口号,在后面的bind函数中传入作为第二个参数,bind成功,就等同于向操作系统说明,只要是向这个IP地址和端口发送的数据,都先经过我listen_sock这里。
这里同时也要注意网络字节序的转换,我们先来看一看sockaddr_in的结构:
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
};
可以看出,port是无符号16位的整型,sin_addr则是4个字节的整型,
而在网络字节序的函数,我们应该选择的就是htons(host to net small )和inet_addr。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);(对无符号long4个字节转换为网络字节序的顺序)
uint16_t htons(uint16_t hostshort);(对无符号short2个字节转换为网络字节序的数据)
uint32_t ntohl(uint32_t netlong);(将4个字节的网络字节序数据转换为当前的主机字节序数据)
uint16_t ntohs(uint16_t netshort);将一个16位(2个字节)由网络字节序转变为主机字节序
in_addr_t inet_addr(const char *cp);:将一个十进制的字符串转换为互联网标准转换成,
int inet_pton(int af, const char *src, void *dst);//效果同inet_ntoa
3.开始监听socket
//3.监听listen_sock套接字
if (listen(listen_sock,5))
{
perror("listen error!");
return -1;
}
一旦开始监听,操作系统就会分配一块儿缓冲区作为连接成功队列,listen的第二个参数决定这这个队列的大小,也叫最大并发连接数。
这个队列非常重要,当一个链接到来时,listen_socket检查队列是否还有位置,有就将任务置入队列,创建一个新的socket与该客户端进行通信。
为什么要建立一个新的newsocket,而不直接用监听socket与客户端通信?
原因其实很简单,我们举个例子,我们去中介哪儿租房,中间见到我们,说,行,现在里屋还有位置,你进去里屋,我安排一个想出租房的人和你聊吧。在这里,我们对应着客户,中介对应着监听socket,里屋对应着链接成功队列,出租房子的人对应newsocket。如果我们和socket直接通信,那就等于和中介谈租房的事情,显然不合适。所以在程序中,监听套接字只处理和我们的建立连接的事务,而不亲自和我们通信,一旦链接建立成功,他就会创建一个新的套接字,也就是newsocket和我们通信。每个不同的客户对应不同的newsocket,大家互不打扰,这也侧面说明每条tcp通信都是1对1的。
为什么每个链接的目的ip和目的端口号都是相同的,服务器还是能每个都处理好呢?
答:注意!我们说的和客户端建立链接用的socket是监听socket,他只负责将这个任务注册进任务队列,并不去确定客户的原ip和原端口,而当这个连接请求进入任务队列后,监听socket会创建一个新的socket(newsocket)来与客户端进行数据交互,同时在任务队列外再创建一块缓冲区,在这个新的缓冲区内,newsocket才会确定客户端的原ip和原端口。每个链接的原ip和原端口是不同的,所以操作系统能够区分清楚各个链接,从而达到与多个客户端通信。
4.建立连接成功
struct sockaddr_in client_addr;
socklen_t size = sizeof(client_addr);
int newsock = accept(listen_sock,(struct sockaddr*)&client_addr,&size);
//阻塞等待式的接收新链接,client_addr接收客户端的地址信息
if (newsock < 0)
{
perror("accept error!");
//注意,此处说明该链接无效,但不影响别的链接,应该重新回到上面再次接收新链接
continue;
}
//接收成功
std::cout<<"get a new connection!"<<std::endl;
建立链接所用的accept函数,是一个阻塞式的函数,服务端创建好链接成功队列后,就开始阻塞式的等待客户端的链接,当有客户端连进来,accept立即捕获这个链接,并返回一个套接字描述符,也就是我们前面说的newsocket。
倘若accept出现错误,不能直接return,要注意!服务器不会因为和一个客户端建立链接失败就直接关闭,而是应该重新等待新的链接,故此处用continue。
5-6 与客户端进行数据通信
while(1)
{
while(1)
{
//5.收发数据
char buff[1024] = {0};
int len = recv(newsock , buff , 1024 , 0);
if (len < 0)//recv返回值小于0时,接收数据错误
{
perror("recv error!");
close(newsock);
continue;
}
else if (len == 0)//recv返回值等于0时,说明对端(客户端)关闭
{
perror("the opposite process is stopped!");
close(newsock);
continue;
}
//运行到此处说明接收成功,接受的数据存放在buff中
printf("client[ %s %d] say: %s\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),buff);
//注意此处对数据要从网络字节序转化为主机字节序
memset(buff,0x00,1024);
std::cin >> buff;
send(newsock,buff,strlen(buff),0);
}
}
这里要注意recv和send函数,udp通信使用的是recvfrom和sendto,我有一篇博客写他们之间的区别,可以参考一下:从tcp,udp链接角度看send和sendto的区别。
如何判断tcp断开链接?
recv:正常接收,返回实际接收长度。 接受失败 返回 -1 对端断开 0
send:正常发送,返回值为所发送数据的总数。
如果对端断开,但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用send(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出 。
所以我们在recv下面添加了判断语句,如果recv返回值为0,那么就close掉当前的newsocket,返回上文继续等待新的客户端。
7关闭socket
close(listen_sock);
服务器停掉前,肯定要关闭监听套接字了。
客户端与服务端逻辑类似,且代码中有加入注释,就不多赘述了。下面放代码
tcp通信程序1.0版本(不能支持多个客户端通信)
tcp_server.cpp
//目标:创建一个基于tcp的简单聊天程序
#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
//思路:
//服务端:
//1.创建套接字listen_socket
//2.为套接字绑定服务器地址
//3.开始监听listen_socket
//4.接收链接成功,使用new_socket
//5.与new_socket收发数据
//6.关闭socket
// /usr/include/netinet/in.h
int main(int argc,char* argv[])
{
if (argc != 3)//判断参数是否足够
{
perror("you should put more paramaters!");
return -1;
}
//1.创建监听套接字listen_sock,参数依次为版本IPv4,流式套接字,默认tcp
int listen_sock = socket(AF_INET,SOCK_STREAM,0);
if (listen_sock < 0)
{
perror("create socket failed!");
return -1;
}
//2.为服务器绑定地址信息,以后凡是向服务器发起连接请求,都会首先让listen_sock处理。
struct sockaddr_in server_addr; //sockaddr_in仅用于IPv4
socklen_t size = sizeof(server_addr);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));//先将命令行第二个参数(端口号)转为整形再转为网络字节序。
server_addr.sin_addr.s_addr = inet_addr(argv[1]);//将命令行第一个参数(ip地址)转为网络字节序。
int ret = bind(listen_sock,(struct sockaddr*)&server_addr,size);
if (ret < 0)
{
perror("bind error!");
return -1;
}
//3.监听listen_sock套接字
if (listen(listen_sock,5))
{
perror("listen error!");
return -1;
}
while(1)
{
//4.接收新链接
struct sockaddr_in client_addr;
socklen_t size = sizeof(client_addr);
int newsock = accept(listen_sock,(struct sockaddr*)&client_addr,&size);//阻塞等待式的接收新链接,client_addr接收客户端的地址信息
if (newsock < 0)
{
perror("accept error!");//注意,此处说明该链接无效,但不影响别的链接,应该重新回到上面再次接收新链接
continue;
}
//接收成功
std::cout<<"get a new connection!"<<std::endl;
//开始用newsock和客户端进行数据通信
while(1)
{
//5.收发数据
char buff[1024] = {0};
int len = recv(newsock , buff , 1024 , 0);
if (len < 0)//recv返回值小于0时,接收数据错误
{
perror("recv error!");
close(newsock);
continue;
}
else if (len == 0)//recv返回值等于0时,说明对端(客户端)关闭
{
perror("the opposite process is stopped!");
close(newsock);
continue;
}
//运行到此处说明接收成功,接受的数据存放在buff中
printf("client[ %s %d] say: %s\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),buff);
//注意此处对数据要从网络字节序转化为主机字节序
memset(buff,0x00,1024);
std::cin >> buff;
send(newsock,buff,strlen(buff),0);
}
}
close(listen_sock);
return 0;
}
tcp_client.cpp
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
//目标:创建一个简单的tcp聊天客户端
//
//1.创建套接字socket
//2.绑定地址信息(无需绑定,绑定可能出错,且我们在服务端可以用recv函数接收到客户端的地址信息)
//3.与服务端建立连接(connect)
//4.向服务端发送数据
//5.接收服务端发来的数据
//6.不聊了关闭套接字
//
//如何判断TCP连接断开?(重点)
//对于接收方来说----recv返回值为0,代表连接断开
//对于发送方来说----每次调用send都会触发连接断开异常,接收到系统发送的SIGPIPE信号,导致进程退出.
//如果不想要发送方退出,我们要在程序开始的时候对SIGPIPE做自定义/忽略处理
int main(int argc , char* argv[])
{
if (argc != 3)//检查是否缺少参数
{
perror("you should put more paramaters!");
return -1;
}
//1创建套接字sockfd
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (sockfd < 0)
{
perror("create socket error!");
return -1;
}
//2不绑定客户端的地址信息
//
//创建一个地址信息块,存储服务端的地址信息,后面connect,send用得上
struct sockaddr_in server_addr;//仅用于IPv4
socklen_t len = sizeof(server_addr);
server_addr.sin_family = AF_INET;//类型
server_addr.sin_port = htons(atoi(argv[2]));//端口是无符号short型,这里要先将参数转为int,再对低2个字节进行网络字节序的转换。
server_addr.sin_addr.s_addr = inet_addr(argv[1]);//inet_addr可以完成字符串的网络字节序转换。
//3.申请与服务端建立连接。
int ret = connect(sockfd, (struct sockaddr *)&server_addr, len);
if (ret < 0)
{
perror("connect error!");
close(sockfd);
return -1;
}
//至此,链接链接建立成功
while (1)
{
//4.向服务端发送数据
char buff[1024] = {0};
scanf("%s",buff);
send(sockfd,buff,strlen(buff),0);
//5.从服务端接收数据
memset(buff,0x00,1024);
int ret = recv(sockfd,buff,1024,0);
if (ret < 0)
{
//客户端接收数据失败
perror("recv error!");
close(sockfd);
return -1;
}
else if (ret ==0)
{
//对端已经关闭
perror("the tcp_server is closed!");
close(sockfd);
return -1;
}
//输出buff的内容,要注意ip和端口从网络字节序到主机字节序的转换
printf("server[%s %d]say: %s\n",inet_ntoa(server_addr.sin_addr),ntohs(server_addr.sin_port),buff);
}
close(sockfd);
return 0;
}
程序的问题
这个程序只能与一个客户端维持正常通信,一旦打开一个新的客户端,能连接但服务端接收不到信息。这是为什么呢?
外层while循环是不断的等待新的客户端连接的到来,但只要有1个客户端进来,就会进入内层循环一直通信,导致外层的accept无法执行,所以客户端根本就不知道有新的链接到来。
解决思路:
1.将服务端改成多进程版本:
可以每当一个连接到来时,就fork()一个子进程,让子进程去完成通信任务,父进程仍然在处理建立链接的任务。我们又知道,当子进程退出,会给父进程发送SIGCHID信号,我们只需要用signal()函数重定义SIGCHID的行为,让他wait一下就可以了。
修改后的server
//目标:创建一个基于tcp的简单聊天程序
#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>
//思路:
//服务端:
//1.创建套接字listen_socket
//2.为套接字绑定服务器地址
//3.开始监听listen_socket
//4.接收链接成功,使用new_socket
//5.与new_socket收发数据
//6.关闭socket
int Communication(int newsock)
{
int pid = fork();
if (pid < 0)
{
perror("fork error!");
return -1;
}
else if (pid == 0)
{
//开始用newsock和客户端进行数据通信
while(1)
{
//5.收发数据
char buff[1024] = {0};
int len = recv(newsock , buff , 1024 , 0);
if (len == 0)//recv返回值小于等于0时,释放套接字后退出
{
std::cout<<"the opposite is closed"<<std::endl;
close(newsock);
exit(-1);
}
else if (len < 0)
{
std::cout<<"recv error"<<std::endl;
close(newsock);
exit(-1);
}
//运行到此处说明接收成功,接受的数据存放在buff中
printf("client say: %s\n",buff);
//注意此处对数据要从网络字节序转化为主机字节序
send(newsock,"hello~",6,0);
}
}
close(newsock);
return 0;
}
void sigcb(int signo)
{
while (waitpid(-1,NULL,WNOHANG) != 0)
{}
}
int main(int argc,char* argv[])
{
signal(SIGCHLD,sigcb);
if (argc != 3)
{
perror("you should put more paramaters!");
return -1;
}
//1.创建监听套接字listen_sock,参数依次为版本IPv4,流式套接字,默认tcp
int listen_sock = socket(AF_INET,SOCK_STREAM,0);
if (listen_sock < 0)
{
perror("create socket failed!");
return -1;
}
//2.为服务器绑定地址信息,以后凡是向服务器发起连接请求,都会首先让listen_sock处理。
struct sockaddr_in server_addr; //sockaddr_in仅用于IPv4
socklen_t size = sizeof(server_addr);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));//先将命令行第二个参数(端口号)转为整形再转为网络字节序。
server_addr.sin_addr.s_addr = inet_addr(argv[1]);//将命令行第一个参数(ip地址)转为网络字节序。
int ret = bind(listen_sock,(struct sockaddr*)&server_addr,size);
if (ret < 0)
{
perror("bind error!");
return -1;
}
//3.监听listen_sock套接字
if (listen(listen_sock,5))
{
perror("listen error!");
return -1;
}
while(1)
{
//4.接收新链接
struct sockaddr_in client_addr;
socklen_t size = sizeof(client_addr);
int newsock = accept(listen_sock,(struct sockaddr*)&client_addr,&size);//阻塞等待式的接收新链接,client_addr接收客户端的地址信息
if (newsock < 0)
{
perror("accept error!");//注意,此处说明该链接无效,但不影响别的链接,应该重新回到上面再次接收新链接
continue;
}
//接收成功
std::cout<<"get a new connection!"<<std::endl;
Communication(newsock);
}
close(listen_sock);
return 0;
}
2.多线程版本
和进程一样,没来一个链接都创建一个线程来执行这个通信任务,将线程分离后也不用担心资源是否回收,其他较多进程版本其实没有太大变化。
//目标:创建一个基于tcp的简单聊天程序
#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>
#include<pthread.h>
//思路:
//服务端:
//1.创建套接字listen_socket
//2.为套接字绑定服务器地址
//3.开始监听listen_socket
//4.接收链接成功,使用new_socket
//5.与new_socket收发数据
//6.关闭socket
int Communication(void* arg)
{
pthread_detach(pthread_self());
int newsock = (int)arg;
while(1)
{
//5.收发数据
char buff[1024] = {0};
int len = recv(newsock , buff , 1024 , 0);
if (len == 0)//recv返回值小于等于0时,释放套接字后退出
{
std::cout<<"the opposite is closed"<<std::endl;
close(newsock);
return 0;
}
else if (len < 0)
{
std::cout<<"recv error"<<std::endl;
close(newsock);
return -1;
}
//运行到此处说明接收成功,接受的数据存放在buff中
printf("client say: %s\n",buff);
//注意此处对数据要从网络字节序转化为主机字节序
send(newsock,"hello~",6,0);
}
close(newsock);
return NULL;
}
int main(int argc,char* argv[])
{
if (argc != 3)
{
perror("you should put more paramaters!");
return -1;
}
//1.创建监听套接字listen_sock,参数依次为版本IPv4,流式套接字,默认tcp
int listen_sock = socket(AF_INET,SOCK_STREAM,0);
if (listen_sock < 0)
{
perror("create socket failed!");
return -1;
}
//2.为服务器绑定地址信息,以后凡是向服务器发起连接请求,都会首先让listen_sock处理。
struct sockaddr_in server_addr; //sockaddr_in仅用于IPv4
socklen_t size = sizeof(server_addr);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));//先将命令行第二个参数(端口号)转为整形再转为网络字节序。
server_addr.sin_addr.s_addr = inet_addr(argv[1]);//将命令行第一个参数(ip地址)转为网络字节序。
int ret = bind(listen_sock,(struct sockaddr*)&server_addr,size);
if (ret < 0)
{
perror("bind error!");
return -1;
}
//3.监听listen_sock套接字
if (listen(listen_sock,5))
{
perror("listen error!");
return -1;
}
while(1)
{
//4.接收新链接
struct sockaddr_in client_addr;
socklen_t size = sizeof(client_addr);
int newsock = accept(listen_sock,(struct sockaddr*)&client_addr,&size);//阻塞等待式的接收新链接,client_addr接收客户端的地址信息
if (newsock < 0)
{
perror("accept error!");//注意,此处说明该链接无效,但不影响别的链接,应该重新回到上面再次接收新链接
continue;
}
//接收成功
std::cout<<"get a new connection!"<<std::endl;
pthread_t tid;
pthread_create(&tid,NULL,Communication,(void*)newsock);
}
close(listen_sock);
return 0;
}
两个版本的比较
多线程版本固然有好处:资源占用少,创建/销毁较多进程版本要小,但是也有不好的地方,倘若一个线程因异常崩溃,整个进程都受到影响,服务器就被搞挂了,所以没有绝对的好与坏,的看需求和应用场景再做决断。