目录
一.预备知识
认识端口号:
1.端口号(port)是传输层协议的内容;
2.端口号是一个2字节16位的整数;
3.端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
4.IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
5.一个端口号只能被一个进程占用;
理解 "端口号" 和 "进程ID"
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
两者没有关系,因为是两个不同的模块,没必要绑定在一起,因为一个是网络层次的,另一个是操作系统层次的,另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定,10086,你打这个电话,并不是同一个人给你服务,就说明了,一个进程可以绑定多个端口号。
理解源端口号和目的端口号:
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号。就是在描述 “数据是谁发的, 要发给谁”
源端口号: 表示该条信息来源于哪个进程。
目的端口号: 表示该条信息去往于哪个机器。
认识TCP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题。
1.传输层协议
2.有连接: 双方在发送网络数据之前必须建立连接,再进行发送
3.可靠传输: 保证数据是可靠并且有序的到达对端,例如发送123、456时123数据先到达,456数据后到达。即使456数据先到达传输层,也会阻塞等待前面的数据123先到达。
4.面向字节流: TCP发送数据的单位是以字节为单位,并且数据没有明显的边界例如:123456数据不会分开
认识UDP协议
这里我们也先对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;,后面再详细讨论。
1.传输层协议
2.无连接: 双方在发送网络数据之前不需要建立连接,直接发送,客服端不用管服务端是否在线
3.不可靠传输: UDP并不会保证数据有序的到达对端
4.面向数据报: UDP不管向应用层还是网络层传递数据都是整条数据
对比一下TCP和UDP
TCP是可靠传输,有连接,字节流
UDP是不可靠传输,无连接,数据报
二.网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
1.发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
2.接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
3.因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
4.TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
5.不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
6.如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
三.socket结构
socket常见API(接口)
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr * address,socklen_t address_len);*
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr*addr,socklen_t addrlen);
常见的套接字有三种,分别为,域间socket,原始socket,网络socket,理论上,是三种应用场景,对应的应该是三套接口,但是不想设计太多接口,就将所有的接口进行统一。
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同.
Sockaddr_in是网络socket,Sockaddr_un是域间socket,统一使用sockaddr,根据前两个字节分辨是哪一个(Sockaddr_in,Sockaddr_un)。
1.IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
2.IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
3.socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
sockaddr 结构
sockaddr 结构用于存储参与(IP)Windows/linux套接字通信的计算机上的一个internet协议(IP)地址。为了统一地址结构的表示方法 ,统一接口函数,使得不同的地址结构可以被bind()、connect()、recvfrom()、sendto()等函数调用。但一般的编程中并不直接对此数据结构进行操作,而使用另一个与之等价的数据结构sockaddr_in。这是由于Microsoft TCP/IP套接字开发人员的工具箱仅支持internet地址字段,而实际填充字段的每一部分则遵循sockaddr_in数据结构,两者大小都是16字节,所以二者之间可以进行切换。
sockaddr_in 结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。
in_addr结构
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。
四.socket编程接口
功能: 打开一个网络通讯端口,打开后应用程序可以像读写文件一样用read/write在网络上收发数据,说白了就是,创建一个套接字,返回值是文件描述符,失败返回-1。
参数:
domain:协议域,对于IPv4, family参数指定为AF_INET。
type:socket的类型,流格式套接字(SOCK_STREAM)、数据报格式套接字(SOCK_DGRAM)。
protocol:协议,当protocol为0时,会自动选择type类型对应的默认协议。
功能: 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端号,bind函数的功能就是将参数sockfd和addr绑定在一起,返回值是成功0,失败-1。
参数:
sockfd:创建的套接字,即socket函数返回的文件描述符,唯一标识一个socket
addr:sockaddr结构体
addrlen:struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
接下来需要详细的讲一下addr这个参数,
bzero (&local sizeof(local)) ;// 将整个结构体清零
local.sin_family = AF_INET;// 设置地址类型为AF_INETlocal.sin_port = htons(_port);// 端口号为_port
local.sin_addr.s _addr= _ip.empty()?INADDR_ANY:inet_addr(_ip.c_str());// 网络地址IP为_ip,因为我们一般是不bind固定ip的,所以这里如果没有具体IP就使用INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址
功能:声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略,返回值:成功返回0,失败返回-1。
参数:
sockfd:创建的套接字
backlog:最多允许客户端处于连接等待状态的个数(通常设置为5)
功能: 在一个套接口接受一个连接,三次握手完成后, 服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来,返回值: 成功返回非负值,失败返回-1。
参数:
sockfd:创建的套接字
addr:addr是一个传出参数,accept()返回时传出客户端的地址和端口号,如果给addr 参数传NULL,表示不关心客户端的地址
addrlen:是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)
功能: 客户端需要调用connect()连接服务器,返回值: connect()成功返回0,出错返回-1。
参数:
sockfd:创建的套接字,即socket函数返回的文件描述符,唯一标识一个socket
addr:sockaddr结构体
addrlen:struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
功能:读取数据,返回值,读取来数据的长度
参数:
sockfd:创建的套接字,即socket函数返回的文件描述符,唯一标识一个socket
buf:一段缓冲区
len:缓冲区长度
flags:默认0,阻塞读取
src_addr:sockaddr结构体,输出型参数,因为我们要关注是谁发给我们的
addrlen:src_addr的大小
功能:发送数据
参数:
sockfd:创建的套接字,即socket函数返回的文件描述符,唯一标识一个socket
buf:一段缓冲区
len:缓冲区长度
flags:默认0
src_addr:sockaddr结构体,存的是你要发给的人的信息
addrlen:src_addr的大小
五.UDP协议实践
首先一个简单的网络通信程序分为服务端和客户端,接下来我们就分别看一下服务端和客户端是如何实现的。
日志文件
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./threadpool.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
if(level== DEBUG) return;
#endif
// va_list ap;
// va_start(ap, format);
// while()
// int x = va_arg(ap, int);
// va_end(ap); //ap=nullptr
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// vprintf(format, args);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
// FILE *fp = fopen(LOGFILE, "a");
printf("%s%s\n", stdBuffer, logBuffer);
// fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
// fclose(fp);
}
服务端udp_server.cc
#include "log.hpp"
#include <iostream>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <memory>
#include <cstdlib>
using namespace std;
class UdpServer
{
public:
UdpServer(uint16_t port,string ip=""):_port(port),_ip(ip),_sock(-1)
{}
bool initServer()
{
_sock= socket(AF_INET,SOCK_DGRAM,0);
if(_sock<0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=_ip.empty()?INADDR_ANY:inet_addr(_ip.c_str());
if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
//done
return true;
}
void Start()
{
char buffer[1024];
for(;;)
{
//注意:
// peer,纯输出型参数
struct sockaddr_in peer;
bzero(&peer, sizeof(peer));
// 输入: peer 缓冲区大小
// 输出: 实际读到的peer
socklen_t len = sizeof(peer);
//start. 读取数据
ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
buffer[s] = 0; // 我们目前数据当做字符串
//1. 输出发送的数据信息
//2. 是谁??
uint16_t cli_port = ntohs(peer.sin_port); //从网络中来的!
std::string cli_ip = inet_ntoa(peer.sin_addr); //4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
}
//分析和处理数据,TODO
//end. 写回数据
sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
}
}
~UdpServer()
{
if(_sock >= 0) close(_sock);
}
private:
uint16_t _port;
string _ip;
int _sock;
};
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(1);
}
// std::string ip = argv[1];
uint16_t port = atoi(argv[1]);
std::unique_ptr<UdpServer> svr(new UdpServer(port));
svr->initServer();
svr->Start();
return 0;
}
接下来我就一步一步地讲解一下上面的代码:
首先说一下UdpServer这个类
类的三个成员:分别是_port端口号,_ipIP地址,_sock套接字
构造函数:UdpServer(uint16_t port,string ip=""),这里主要就是ip给缺省,因为服务器一般不绑定固定ip,这样我们可以从任意IP中获取数据
析构函数:这个就是关闭套接字
initServer()函数:这个函数的作用就是创建套接字并且绑定IP和port,创建套接字就是使用socket这个函数,绑定是使用bind这个函数(函数如何使用在上面都有介绍)
Start()函数:这个函数的作用就是读取数据和发送数据,因为服务器一般是不退出的,所以这里我们是写成死循环的,读取数据使用的是recvfrom,发送数据使用的是sendto
主函数:首先usage函数就是一个手册,告诉用户如何使用,然后就是创建一个UdpServer的类对象,然后调用initServer()函数和Start()函数即可
客户端udp_client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
<< std::endl;
}
uint16_t serverport = 0;
std::string serverip;
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
serverport = atoi(argv[2]);
serverip = argv[1];
std::string message;
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
char buffer[1024];
while(true)
{
std::cout << "请输入你的信息# ";
std::getline(std::cin, message);
if(message == "quit") break;
// 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof server);
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);
if(s > 0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
代码讲解:
主函数:第一步有一个usage()函数,就是使用手册,然后就是创建套接字,然后就是客户端一般也是不退出的,在一个死循环里发送消息给服务器,并且接收服务器发来的消息。
多线程版:
thread.hpp:
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>
// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *);
class ThreadData
{
public:
void *args_;
std::string name_;
};
class Thread
{
public:
Thread(int num, fun_t callback, void *args) : func_(callback)
{
char nameBuffer[64];
snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);
name_ = nameBuffer;
tdata_.args_ = args;
tdata_.name_ = name_;
}
void start()
{
pthread_create(&tid_, nullptr, func_, (void*)&tdata_);
}
void join()
{
pthread_join(tid_, nullptr);
}
std::string name()
{
return name_;
}
~Thread()
{
}
private:
std::string name_;
fun_t func_;
ThreadData tdata_;
pthread_t tid_;
};
客户端:
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#include "thread.hpp"
// 发现:无论是多线程读还是写,用的sock都是一个,sock代表就是文件,UDP是全双工的-> 可以同时进行收发而不受干扰
uint16_t serverport = 0;
std::string serverip;
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
<< std::endl;
}
static void *udpSend(void *args)
{
int sock = *(int *)((ThreadData *)args)->args_;
std::string name = ((ThreadData *)args)->name_;
std::string message;
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while (true)
{
std::cerr << "请输入你的信息# "; //标准错误 2打印
std::getline(std::cin, message);
if (message == "quit")
break;
// 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
}
return nullptr;
}
static void *udpRecv(void *args)
{
int sock = *(int *)((ThreadData *)args)->args_;
std::string name = ((ThreadData *)args)->name_;
char buffer[1024];
while (true)
{
memset(buffer, 0, sizeof(buffer));
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}
}
}
// ./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
serverport = atoi(argv[2]);
serverip = argv[1];
// client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind
// client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->
// client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢??
// client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)
std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock));
std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock));
// sender->name();
sender->start();
recver->start();
sender->join();
recver->join();
close(sock);
return 0;
}
六.tcp协议实践
客户端:tcp_client.cc
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[1]);
return 0;
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
int sock = 0;
bool alive = false;
std::string line;
while (true)
{
if (!alive)
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
}
struct sockaddr_in server;
bzero(&server, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
// client 要不要bind呢?不需要显示的bind,但是一定是需要port
// 需要让os自动进行port选择
// 连接别人的能力!
// conect连接
if (connect(sock, (struct sockaddr *)&server, sizeof server))
{
// 连接失败
exit(2);
}
// 连接到了
alive = true;
}
std::cout << "请输入# ";
std::getline(std::cin, line);
if (line == "quit")
break;
// send recv和udp中的sendto recvfrom 是一样的
ssize_t s = send(sock, line.c_str(), line.size(), 0);
if (s > 0)
{
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof buffer - 1, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << "server 回显#" << buffer << std::endl;
}
else if (s == 0)
{
alive = false;
close(sock);
}
}
else
{
alive = false;
close(sock);
}
}
return 0;
}
服务端:tcp_server.cc tcp_server.hpp
#include"tcp_server.hpp"
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
usage(argv[1]);
return 0;
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<TcpServer> svr(new TcpServer(port));
svr->initServer();
svr->start();
return 0;
}
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <ctype.h>
// static void service(int sock, const std::string &clientip,
// const uint16_t &clientport, const std::string &thread_name)
static void service(int sock, const std::string &clientip,
const uint16_t &clientport)
{
// echo server
// 同时在线10人
// 所以,我们一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
// 后面有其他方案!
char buffer[1024];
while (true)
{
// read && write 可以直接被使用!
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; // 将发过来的数据当做字符串
// std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else if (s == 0) // 对端关闭连接
{
// logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
break;
}
else
{
// logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
break;
}
write(sock, buffer, strlen(buffer));
}
close(sock);
}
class TcpServer
{
private:
// listen函数第二个参数
const static int gbacklog = 20;
public:
TcpServer(uint16_t port, std::string ip = "")
: _port(port), _ip(ip), _listensock(-1)
{
}
void initServer()
{
// 1.创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 创建失败
}
// 2.bind绑定
struct sockaddr_in local;
bzero(&local, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
if (bind(_listensock, (struct sockaddr *)&local, sizeof local))
{
// bind失败
}
// 因为tcp是面向连接的,所以需要使用listen,先建立连接
if (listen(_listensock, gbacklog) < 0)
{
// listen连接失败
}
return;
}
void start()
{
while (true)
{
// 4.获取连接
struct sockaddr_in src;
bzero(&src, sizeof src);
socklen_t len = sizeof src;
int servicesock = accept(_listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
// 没有获取到
continue;
}
// 获取连接成功
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
// 执行任务
// 开始进行通信服务啦
// version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
// 很显然,是不能够直接被使用的! -- 为什么? 单进程
service(servicesock, client_ip, client_port);
// version 2.0 -- 多进程版 --- 创建子进程
// 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?1 0
// pid_t id = fork();
// assert(id != -1);
// if(id == 0)
// {
// // 子进程, 子进程会不会继承父进程打开的文件与文件fd呢?1, 0
// // 子进程是来进行提供服务的,需不需要知道监听socket呢?
// close(_listensock);
// service(servicesock, client_ip, client_port);
// exit(0); // 僵尸状态
// }
// close(servicesock); // 如果父进程关闭servicesock,会不会影响子进程??下节课
// 父进程
// waitpid();
}
return;
}
~TcpServer()
{
if (_listensock >= 0)
{
close(_listensock);
}
}
private:
uint16_t _port;
std::string _ip;
int _listensock;
};
因为tcp和udp大致一样,所以就不过多解释了。
七.TCP协议通讯流程
TCP协议通讯的前提是先将客户端和服务端连接起来,这里的连接是指什么呢?
客户端连接服务器的时候,本质上是连接了服务器的操作系统(协议栈)模块。服务端和客户端是1:n的关系,所以一定会有多个客户端去连接一个服务器OS。
此时服务器OS上会有大量的客户端连接。服务器OS需要将所有连接管理起来(先描述,再组织)。
所谓的连接本质就是在双方OS内,维护对应的数据结构,建立了连接,后序也要维护连接,所以,建立连接是有成本的,消耗了时间和空间。断开连接的本质:释放双方建立好的数据结构
下图是基于TCP协议的客户端/服务器程序的一般流程:
服务器初始化:
- 调用socket, 创建文件描述符;
- 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
- 调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
- 调用socket, 创建文件描述符;
- 调用connect, 向服务器发起连接请求;
- connect会发出SYN段并阻塞等待服务器应答; (第一次)
- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
- 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
- 这个建立连接的过程, 通常称为 三次握手;
数据传输的过程:
- 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
- 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
- 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
- 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
- 客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
- 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
- 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
- read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
- 客户端收到FIN, 再返回一个ACK给服务器; (第四次)
- 这个断开连接的过程, 通常称为 四次挥手
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
- 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
- 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
TCP 和 UDP 对比:
- 可靠传输 vs 不可靠传输
- 有连接 vs 无连接
- 字节流 vs 数据报