套接字编程TCP
- TCP的编程流程
- TCP的接口
- TCP的代码(单线程、多进程、多线程代码)
- 单线程
- 多进程
- 多线程
TCP的编程流程
TCP的编程流程:大致可以分为五个过程,分别是准备过程、连接建立过程、获取新连接过程、消息收发过程和断开过程。
1.准备过程:服务端和客户端需要创建各自的套接字,除此之外服务端还需要绑定自己的地址信息和进行监听。注意:服务端调用listen函数后,处理监听状态,这时由socket函数返回的描述符被称为”监听套接字“,本质上是一个文件描述符,但是客户端没有”监听套接字‘这个概念。
2.连接建立过程:三次握手的过程,请注意,三次握手需要在服务端开始监听之后才能进行的,并且在应用层只需要程序员调用connect接口就可以了,剩下的三次握手的具体过程是在操作系统内核进行的,无需程序员干预
3.获取新连接过程:当连接建立好之后,建立好的连接会被放入已完成连接队列,在服务端只需要调用accept接口获取新的套接字即可。注意:如果没有调用accept接口,新的套接字不会被拿到应用层。也就没有真正意义上的建立连接
4.消息收发过程:当本端给对端发送消息时,将消息发送后,对端会回复一个确认消息。这个确认消息并不是程序员调用发送接口发送的,而是TCP协议自己发送的。
5.关闭过程:调用close接口后,就会关闭本端的新连接套接字。
对于连接建立过程来说:当某个客户端发送了连接请求后,还处于正在建立连接的过程中,就会处于未完成连接队列,而已经完成了建立连接这个过程的,就处于已完成连接队列。accept函数就是从已完成连接队列中拿新连接套接字的。
TCP的接口
对于创建套接字和绑定的操作接口,在【Linux】网络---->套接字编程UDP中已介绍,这里就只介绍和UDP不同的接口。
监听:当TCP服务端调用listen函数时,属于listen状态,标志着服务端可以正常接收客户端的请求了。
int listen(int sockfd, int backlog);
参数:
sockfd:套接字描述符,也就是socket函数的返回值,被称为”监听套接字“
backlog:实际含义是:已完成连接队列的大小。引申含义是:TCP并发连接数:TCP瞬时能处理TCP连接的最大数量(瞬时没有调用accept函数)。当服务端不进行accept,服务端最多能建立连接的个数为backlog+1。
可以通过/proc/sys/net/ipv4/tcp_max_syn_backlog修改未完成连接队列的大小。
注意:TCP并发连接数不等同于TCP服务端最多能够处理多少个TCP连接
问题:那么TCP服务端到底最多能够处理多少个TCP连接呢?
这个问题其实就是TCP服务端到底能够打开多少个文件描述符?
而到底能够打开多少个文件描述符,实际上是可以通过命令查看的,也可以通过命令修改,但是当超过某个值后,硬件就限制了其无法真正的打开n多个文件描述符。
accept获取新连接套接字
//这个函数是一个阻塞函数,如果没有已完成连接的连接,则阻塞等待,如有则返回
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd:套接字描述符
addr:地址信息结构体,描述客户端地址信息的结构体
addrlen:地址信息长度
返回值:成功返回新连接的套接字,失败返回-1
注意:返回的新连接套接字是后续客户端和服务端进行通信的。而监听套接字是在准备阶段,连接建立阶段和获取连接阶段使用的。
意味着:监听套接字负责接收来自各个客户端连接请求的,而连接完成,服务端进行accept之后,会为每一个客户端创建一个独有的专门的新套接字。服务端通过新连接套接字和对应的客户端进行通信。
由客户端调用connect函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:套接字描述符(客户端)
addr:描述服务端的地址信息(服务端的ip和端口)
addrlen:地址信息长度
返回值:成功返回0,失败返回-1
注意:connect函数也可以绑定客户端的地址信息。(如果客户端没有进行绑定)
发送和接收
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
参数:
sockfd:套接字描述符
buf:发送buf指向空间的内容
len:发送数据的长度
flags:0阻塞发送
注意:返回值很重要,成功返回发送的字节数,失败返回-1。
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
参数:
sockfd:新连接套接字描述符
服务端:新连接套接字
客户端:socket 函数的返回值
buf,将接收到的数据存放在buf指定的空间,空间需要提前开辟好
len:期望接收的字节个数
flags:0阻塞接收
注意:返回值很重要,成功返回接收到的字节个数,接收失败返回-1, 对端关闭连接0
TCP的代码(单线程、多进程、多线程代码)
单线程
注:由于未使用多路转接,所以无法使用单线程完成—>既要accept又要send和recv,因此这个代码是有bug的。
服务端代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
/*
* 1.创建套接字
* 2.绑定地址信息
* 3.监听
* 4.通信
* 5.关闭
* */
int listen_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(listen_sockfd < 0)
{
perror("socket");
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(27878);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(listen_sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return 0;
}
ret = listen(listen_sockfd, 5);
if(ret < 0)
{
perror("listen_sockfd");
return 0;
}
while(1)
{
int newsockfd = accept(listen_sockfd, NULL, NULL);
if(newsockfd < 0)
{
return 0;
}
printf("newsockfd:%d\n", newsockfd);
//通信过程
char buf[1024] = {0};
ssize_t r_size = recv(newsockfd, buf, sizeof(buf)-1, 0);
if(r_size < 0)
{
continue;
}
else if(r_size == 0)
{
printf("%d connect shutdown\n", newsockfd);
close(newsockfd);
}else{
printf("client say : %s\n", buf);
send(newsockfd, buf, strlen(buf), 0);
}
}
while(1)
{
sleep(1);
}
return 0;
}
客户端代码:
#include<stdio.h>
#include<unistd.h>
#include<iostream>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
int main()
{
/*
* 1.创建套接字
* 2.发起连接
* */
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd < 0)
{
perror("socket");
return 0;
}
/*要描述服务端的ip和port*/
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(27878);
addr.sin_addr.s_addr = inet_addr("10.0.12.14");
int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("connect");
return 0;
}
while(1)
{
char buf[1024] = {0};
printf("please enter: ");
fflush(stdout);
cin >> buf;
send(sockfd, buf, strlen(buf), 0);
memset(buf, '\0', sizeof(buf));
ssize_t r_size = recv(sockfd, buf, sizeof(buf)-1, 0);
if(r_size < 0)
{
continue;
}
else if(r_size == 0)
{
printf("%d connect shutdown\n", sockfd);
close(sockfd);
}else{
printf("server recall ------- %s\n", buf);
}
}
return 0;
}
多进程
注:由于未使用多路转接,所以对于服务端来说,只能通过创建多个进程来实现父进程accept,子进程和客户端进行通信
服务端代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
void signalcallback(int sig)
{
wait(NULL);
}
int main()
{
signal(SIGCHLD, signalcallback);
/*
* 1.创建套接字
* 2.绑定地址信息
* 3.监听
* 4.通信
* 5.关闭
* */
int listen_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(listen_sockfd < 0)
{
perror("socket");
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(27878);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(listen_sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return 0;
}
ret = listen(listen_sockfd, 5);
if(ret < 0)
{
perror("listen_sockfd");
return 0;
}
while(1)
{
int newsockfd = accept(listen_sockfd, NULL, NULL);
if(newsockfd < 0)
{
return 0;
}
printf("newsockfd:%d\n", newsockfd);
/*
* 1.创建子进程
* 2.子进程和客户端通信
* */
//通信过程
pid_t pid = fork();
if(pid < 0)
{
close(newsockfd);//客户端通过接收的返回值可以感知到连接断开
continue;
}else if(pid == 0)
{
//子进程进行通信
close(listen_sockfd);
while(1)
{
char buf[1024] = {0};
ssize_t r_size = recv(newsockfd, buf, sizeof(buf)-1, 0);
if(r_size < 0)
{
continue;
}
else if(r_size == 0)
{
printf("%d connect shutdown\n", newsockfd);
close(newsockfd);
exit(1); //结束子进程
}else{
}
printf("client say : %s\n", buf);
}
}
else{
//父进程
//自定义SIGCHLD信号,让父进程可以继续accept
}
}
return 0;
}
多线程
注:由于未使用多路转接,因此除了使用多进程之外,还可以使用多线程进行,让主线程进行accept,工作线程和客户端通信。
服务端代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#include<sys/wait.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
struct NewSockfd
{
int _sockfd;
};
void* thread_worker(void* arg)
{
struct NewSockfd* wokerfd = (struct NewSockfd*)arg;
int newsockfd = wokerfd->_sockfd;
pthread_detach(pthread_self());
//通信过程
while(1)
{
char buf[1024] = {0};
ssize_t r_size = recv(newsockfd, buf, sizeof(buf)-1, 0);
if(r_size < 0)
{
continue;
}
else if(r_size == 0)
{
printf("%d connect shutdown\n", newsockfd);
close(newsockfd);
delete wokerfd;
pthread_exit(NULL); //结束线程
}else{
printf("client say : %s\n", buf);
send(newsockfd, buf, strlen(buf), 0);
}
return NULL;
}
void signalcallback(int sig)
{
wait(NULL);
}
int main()
{
signal(SIGCHLD, signalcallback);
/*
* 1.创建套接字
* 2.绑定地址信息
* 3.监听
* 4.通信
* 5.关闭
* */
int listen_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(listen_sockfd < 0)
{
perror("socket");
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(27878);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(listen_sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return 0;
}
ret = listen(listen_sockfd, 5);
if(ret < 0)
{
perror("listen_sockfd");
return 0;
}
while(1)
{
int newsockfd = accept(listen_sockfd, NULL, NULL);
if(newsockfd < 0)
{
return 0;
}
printf("newsockfd:%d\n", newsockfd);
/*
* 1.创建工作线程
* 2.工作线程和客户端通信
* */
struct NewSockfd* fd = new struct NewSockfd;
fd->_sockfd = newsockfd;
pthread_t tid;
int ret = pthread_create(&tid, NULL, thread_worker, (void*)fd);
if(ret < 0)
{
close(newsockfd);
continue;
}
//通信过程
}
}
return 0;
}