一、TCP服务器的特点
面向连接,可靠传输,字节流服务(适用安全性高,信息质量要求高的传输:文件传输)
先连接再传输。 数据传输灵活。
缺点:传输速度较低,数据可能粘包。
二、TCP服务器的建立流程
socket -> bind -> listen -> connect -> accept -> recv/send -> close
可以看出TCP连接的建立比起UDP多了三步分别是 listen(监听),connect(请求连接),accept(接受连接)
那我们就先看一下man手册中是怎么说的#
listen():
listen接口的描述中贴心的讲解了TCP连接的建立步骤: #“男人”真是贴心啊,问一答三可还行/xyx
首先,使用套接字(2)创建套接字。
接下来,一个接收传入连接的意愿和传入连接的队列限制由listen()指定。
最后,使用accept(2)接受连接。listen()调用只应用于SOCK_STREAM类型的套接字。
connect():
如果套接字类型为SOCK_STREAM(即流式套接字),则此调用将尝试连接到另一个套接字。另一个套接字由地址指定,地址是套接字通信空间中的地址。
通常,流套接字可以成功connect()只有一次;数据报套接字可以多次使用connect()来更改它们的关联。
accept():
accept的描述篇幅很长,这里就不贴了,总结一下是下面几点:
注意这个坑!
man:If no pending connections are present on the queue, and the socket is not marked as non-blocking, accept() blocks the caller until a connection is present.
我第一次编写TCP服务器时,就栽到这里了。意思是说如果,如果没有新连接建立,这个函数会一直阻塞在这里等待新连接。
这就是说,如果不进行特殊处理的话,调用accept接口,将会阻塞你的服务器与客户端间的交互。
下面贴上我的接口封装和我栽倒的V0版服务器。
#ifndef __M_TCPSOCK_H__
#define __M_TCPSOCK_H__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define CHECK_RET(q) if ((q) == false) {return -1;}
class TcpSocket
{
private:
int _sockfd;
public:
TcpSocket():_sockfd(-1){}
//socket
bool CreateSock()
{
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(_sockfd < 0)
{
perror("Socket Create Failed!");
return false;
}
return true;
}
//bind
bool BindAddr(std::string &ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0)
{
perror("Bind Failed!");
return false;
}
return true;
}
//connect
bool Connect(std::string &ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0)
{
perror("connect failed");
return false;
}
return true;
}
//listen
bool StartListen(int max = 5)
{
int ret = listen(_sockfd, max);
if(ret < 0)
{
perror("Listen error");
return false;
}
return true;
}
//accept
bool Accept(TcpSocket *sock, sockaddr_in *cliaddr = NULL)
{
socklen_t len = sizeof(sockaddr_in);
sockaddr_in addr;
int newsockfd = accept(_sockfd, (sockaddr*)&addr, &len);
if(newsockfd < 0)
{
perror("accept error");
return false;
}
sock->_sockfd = newsockfd;
if(cliaddr != NULL)
memcpy(cliaddr, &addr, len);
return true;
}
//recv
ssize_t Recv(char* buf)
{
ssize_t ret = recv(_sockfd, buf, 1023, 0);
if(ret < 0)
{
perror("recv error");
return -1;
}else if(ret == 0)
{
printf("peer shutdown\n");
}
return ret;
}
//send
ssize_t Send(const char* buf)
{
size_t len = strlen(buf);
ssize_t ret = send(_sockfd, buf, len, 0);
if(ret < 0)
{
perror("send error");
return -1;
}
return ret;
}
//close
bool Close()
{
close(_sockfd);
_sockfd = -1;
return true;
}
};
#endif
//tcp_ser_V0
#include "TCPSOCK.h"
int main(int argc, char* argv[])
{
if (argc != 3) {
printf("Usage: ./tcp_srv ip port\n");
return -1;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket sock;
CHECK_RET(sock.CreateSock());
CHECK_RET(sock.BindAddr(ip, port));
CHECK_RET(sock.StartListen(5));
while(1){
TcpSocket clisock;
if(sock.Accept(&clisock) == false){
continue;
}
char buf[1024] = {0};
clisock.Recv(buf);
printf("cli:%s\n",buf);
memset(buf, 0x00, 1024);
printf("ser:");
//fflush(stdout);
scanf("%s", buf);
clisock.Send(buf);
}
sock.Close();
return 0;
}
这个V0版的服务端写法存在的问题就是;一个连接建立后,处理完一波请求之后,程序被阻塞在accept(),就无法再处理当前客户端的请求。
所以,我提出了两种解决方案。一种是通过进程创建,创建一个子进程,用这个子进程创建一个处理连接请求的孙子进程。
子进程创建完孙子进程之后直接退出,目的是让孙子进程成为孤儿进程,能够在处理完连接请求之后自己退出。
改动如下:
//tcp_ser_VProc
while(1){
TcpSocket clisock;
if(sock.Accept(&clisock) == false){
continue;
}
if(fork() == 0){
if(fork() == 0){
while(1){
char buf[1024] = {0};
clisock.Recv(buf);
printf("cli:%s\n",buf);
memset(buf, 0x00, 1024);
printf("ser:");
//fflush(stdout);
scanf("%s", buf);
clisock.Send(buf);
}
clisock.Close();
}else{
exit(0);
}
}
clisock.Close();
wait(NULL);
}
还有一种是通过多线程的方式,每一个连接为其创建一个线程去处理,改动如下:
//tcp_ser_VThread
void *thr_start(void *arg)
{
TcpSocket *clisock = (TcpSocket*)arg;
while(1){
char buf[1024] = {0};
clisock->Recv(buf);
printf("cli:%s\n",buf);
memset(buf, 0x00, 1024);
printf("ser:");
//fflush(stdout);
scanf("%s", buf);
clisock->Send(buf);
}
clisock->Close();
delete clisock;
return NULL;
}
while(1){
TcpSocket *clisock = new TcpSocket();
if(sock.Accept(clisock) == false){
continue;
}
pthread_t tid;
pthread_create(&tid, NULL, thr_start, (void*)clisock);
pthread_detach(tid);
}
PS:还有线程池实现的版本,改日重开一贴详细分析。
以及三次握手,四次挥手的过程,也会重开一贴专门分析。