本文总结对TCP协议的学习以及对比c++与go语言在socket网络编程中的实现。具体而言从TCP以及socket编程原理出发。然后分别实现了基于c++以及go语言的网络通信并发服务器以及客户端。
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
控制位:
ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。
如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。
因为 TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。
简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。
所以我们可以知道,建立一个 TCP 连接是需要客户端与服务端达成上述三个信息的共识。
Socket:由 IP 地址和端口号组成
序列号:用来解决乱序问题等
窗口大小:用来做流量控制
socket是计算机网络中一种通信机制,它可以用于在不同的进程之间进行数据交换。在socket机制中,服务器和客户端之间建立连接,然后它们可以通过这个连接相互发送数据。
Socket的基本概念
IP地址:用于识别Internet上的主机,IPv4地址通常由4个8位数字组成。
端口号:用于标识一个进程,01023是系统保留端口,102465535为动态端口。
协议:TCP协议提供面向连接的服务,确保数据传输的可靠性;UDP协议则提供无连接服务,更适合实时应用。
Socket的类型
Socket有两种类型:
流式Socket(SOCK_STREAM):使用面向连接的TCP协议进行通信。
数据报式Socket(SOCK_DGRAM):使用无连接的UDP协议进行通信。
步骤:
服务端和客户端初始化 socket,得到文件描述符;
服务端调用 bind,将 socket 绑定在指定的 IP 地址和端口;
服务端调用 listen,进行监听;
服务端调用 accept,等待客户端连接;
客户端调用 connect,向服务端的地址和端口发起连接请求;
服务端 accept 返回用于传输的 socket 的文件描述符;
客户端调用 write 写入数据;服务端调用 read 读取数据;
客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。
这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。
对于一个网络通讯服务器,我们需要定义三个东西。
1、定义处理消息的回调函数。
2、定义用来并发执行的连接函数。
3、定义服务器启动函数,用来创建socket以及绑定端口号。
流程就是启动函数结构ip以及端口号还有执行消息处理的回调函数。
所以头文件如下:
typedef void* (*onMessageFunctionPtr)(void*);
class CPPServer {
private:
/* data */
int server_socket;
void ConnectMultiThread(onMessageFunctionPtr);
public:
void StartServer(string ip, int port, onMessageFunctionPtr);
};
1、网络编程中,需要使用套接字进行通信。在服务端,需要创建一个套接字对象。可以使用 socket() 函数来创建一个新的套接字。
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
// 创建失败的处理,可以进行记录或输出错误信息等操作。
LOG_ERR("server_socket create err!!");
}
2、绑定地址和端口,以及定义协议族
struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr(ip.c_str());
server_address.sin_port = htons(port);
int result = bind(server_socket, (struct sockaddr*)&server_address,
sizeof(server_address));
if (result == -1) {
// 绑定失败的处理,可以进行记录或输出错误信息等操作。
LOG_ERR("bind server_socket err!!");
return;
}
3、一旦套接字绑定到了地址和端口上,就可以开始监听连接请求。可以使用 listen()函数来监听连接请求。并且转到连接处理函数。最后关闭套接字释放资源。
result = listen(server_socket, 5);
if (result == -1) {
// 监听失败的处理,可以进行记录或输出错误信息等操作。
LOG_ERR("listen err!!");
return;
}
LOG_INFO("start service at ip:%s port:%d", ip.c_str(), port);
ConnectMultiThread(onMessage);
close(server_socket);
一个死循环不断接受来自客户端的连接,每一次连接成功之后就会开启一个线程取处理客户端的信息。
while (1) {
struct sockaddr_in client_address;
int len = sizeof(client_address);
int newsockfd = accept(server_socket, (struct sockaddr*)&client_address,
(socklen_t*)&len);
if (newsockfd == -1) {
LOG_ERR("accept err!!");
}
char clientIP[16];
inet_ntop(AF_INET, &client_address.sin_addr.s_addr, clientIP,
sizeof(clientIP));
LOG_INFO("============= new client connet ip is %s,port is %d", clientIP,
client_address.sin_port);
pthread_t tid;
int ret = pthread_create(&tid, NULL, onMessage, (void*)&newsockfd);
if (ret != 0) {
// char* errstr = strerror(ret);
LOG_ERR("pthread_create err!!");
}
}
首先定义信息处理回调函数。这里就简单得把客户端发来的消息传回去。注意函数的类型要与服务器头文件定义的类型一致。
void* onMessage(void* arg) {
int newsockfd = *(int*)arg;
while (true) {
char recvBuf[1024] = {0};
int n = read(newsockfd, recvBuf, sizeof(recvBuf));
if (n < 0) {
LOG_ERR("ERROR reading from socket");
break;
} else if (n == 0) {
LOG_INFO("client close...");
break;
} else {
LOG_INFO("<<<<<<== server get client%d data:%s ", newsockfd, recvBuf);
}
// 向客户端发送数据
n = write(newsockfd, recvBuf, sizeof(recvBuf));
LOG_INFO("==>>>>>> server send client%d data:%s ", newsockfd, recvBuf);
if (n < 0) {
LOG_ERR("ERROR writing to socket");
}
} // 关闭连接
close(newsockfd);
}
接下来就是启动服务器
int main(int argc, char** argv) {
if (argc < 3) {
cerr << "command invalid! example: ./ChatServer 127.0.0.1 6000" << endl;
exit(-1);
}
char* ip = argv[1];
uint16_t port = atoi(argv[2]);
CPPServer server;
server.StartServer(ip, port, onMessage);
return 0;
}
客户端一共需要实现三个函数,一个是连接,一个是监听,一个是发送消息函数。
class CPPClient {
private:
/* data */
public:
int Connet(char* ip, int port);
void ListenServerRespond(int fd);
void SendMessage(int fd);
};
传入ip以及端口号,首先创建套接字然后连接服务器。
int CPPClient::Connet(char* ip, int port) {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
LOG_ERR("client socket create err!!");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(port);
int ret = connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (ret == -1) {
LOG_ERR("connect err!!");
exit(-1);
}
LOG_INFO("===================client connect %s:%d success!!", ip, port);
return fd;
}
启动一个线程,进行监听服务器传回来的消息。
void* ListenFunc(void* arg) {
char recvBuf[1024];
int fd = *(int*)arg;
while (1) {
int len = read(fd, recvBuf, sizeof(recvBuf));
if (len == -1) {
exit(-1);
} else if (len > 0) {
LOG_INFO("<<<<<<== client get data:%s", recvBuf);
} else if (len == 0) {
// 表示服务器端断开连接
LOG_INFO("server closed..");
exit(1);
break;
}
}
}
void CPPClient::ListenServerRespond(int fd) {
pthread_t tid;
int ret = pthread_create(&tid, NULL, ListenFunc, (void*)&fd);
if (ret != 0) {
// char* errstr = strerror(ret);
LOG_ERR("pthread_create err!!");
}
}
死循环 对套接字进行写入信息。
void CPPClient::SendMessage(int fd) {
while (1) {
std::string message;
std::getline(std::cin, message);
LOG_INFO("==>>>>>> client send data:%s", message.c_str());
write(fd, message.c_str(), message.size());
}
close(fd);
}
int main(int argc, char** argv) {
if (argc < 3) {
cerr << "command invalid! example: ./ChatServer 127.0.0.1 6000" << endl;
exit(-1);
}
char* ip = argv[1];
uint16_t port = atoi(argv[2]);
CPPClient client;
int fd = client.Connet(ip, port);
// 发一个线程出去专门接受 服务器的消息 线程传入的是fd
client.ListenServerRespond(fd);
// 3. 通信
client.SendMessage(fd);
// // 关闭连接
close(fd);
return 0;
}
因为go语言内置了网络编程相关的库和方法(如net/http包和net包),相比于c++go语言实现并发服务器就简单许多。
首先定义结构体,以及处理信息的回调函数。还有初始化服务器的函数。
type onMessageFunction func(net.Conn)
// 创建Server结构体 包含 ip和 端口号
type Server struct{
Ip string
Port int
onMessage onMessageFunction
}
// server的初始化函数 构造函数
func NewServer(ip string,port int,funcc onMessageFunction)*Server {
server:=&Server{
Ip:ip,
Port:port,
onMessage:funcc,
}
return server
}
然后就是启动。Go 的 net 包提供了实现 TCP 协议所需的所有方法,包括创建一个 TCP 服务器、连接到远程服务器以及在本地和远程主机之间传输数据。
func (this *Server)Start() {
// socket Listeb
fmt.Println("===================== server start!! ======================")
listener,err:=net.Listen("tcp",fmt.Sprintf("%s:%d",this.Ip,this.Port))
if err!=nil {
fmt.Println("listen err:",err)
return
}
defer listener.Close()
fmt.Println("start service at ip:",this.Ip," port:",this.Port)
// 启动对BroadMessageC 的监听
// accept 接受客户端连接
for {
conn,err:=listener.Accept()
if err!=nil {
fmt.Println("accept err:",err)
continue
}
fmt.Println("============new client!! ======================")
// 做出来 通过groutine执行 开一个协程出去 do handler
go this.onMessage(conn)
}
}
启动首先定义处理消息的回调函数,然后将其传进,server的初始化函数中。
func Handler(conn net.Conn) {
fmt.Println("gett !!!",)
buf:=make([]byte,4096)
for{
// n不停的监控 客户端发来的信息 如果发来的信息 n==0 那么就表示这个客户端下线了
// n 传过来的是长度
n,err:=conn.Read(buf)
if n==0{
break
}
if err!=nil&&err!=io.EOF{
fmt.Println("err",err)
}
msg:=string(buf[:n])
fmt.Printf("<<<<<== server get client message:%s \n",msg)
fmt.Printf("=>>>>>> server sent client message:%s \n",msg)
conn.Write([]byte(msg))
}
conn.Close()
}
func main() {
ip:=os.Args[1]
port,_:=strconv.Atoi(os.Args[2])
server:=server.NewServer(ip,port,Handler)
server.Start()
}
首先定义一个 client 结构体包含了服务器 ip 地址、端口号、连接对象和客户端名称四个属性,还定义了它的两个方法:listenresponse 和 sendmessage,用来监听服务器发送的消息以及向服务器发送消息。其中,listenresponse 方法使用 net.conn 的 read 函数来读取服务器发送的消息,并输出到标准输出流中。newclient 函数则负责创建一个新的客户端连接,使用 net.dial 函数连接到指定的服务器地址和端口上,返回一个新的 client 对象。
type Client struct{
ServerIp string
ServerPort int
Name string
conn net.Conn
}
func (this *Client)ListenResponse() {
//一旦有数据 就输出到标准输出删
for{
buf:=make([]byte,4096)
n,err:=this.conn.Read(buf)
if n==0{
return
}
if err!=nil&&err!=io.EOF{
fmt.Println("err",err)
}
msg:=string(buf)
fmt.Println("<<<<<<== client get data",msg)
}
}
func NewClient(serverIp string,serverPort int)*Client {
client:=&Client{
ServerIp:serverIp,
ServerPort:serverPort,
}
// 连接server
conn,err:=net.Dial("tcp",fmt.Sprintf("%s:%d",serverIp,serverPort))
if err!=nil{
fmt.Println("err",err)
return nil
}
client.conn=conn
return client
}
func(this *Client)SendMessage()bool{
for{
var data string
fmt.Scanln(&data)
_,err:=this.conn.Write([]byte(data))
fmt.Println("==>>>>>> client send data",data)
if err!=nil{
fmt.Println("err",err)
return false;
}
}
return true
}
启动根据命令行参数获取服务器的 ip 地址和端口号,然后使用 newclient 函数创建一个新的客户端连接对象
func main() {
ip:=os.Args[1]
port,_:=strconv.Atoi(os.Args[2])
client:=NewClient(ip,port)
if client==nil{
fmt.Println("========> 连接失败...",)
return
}
// 监听
go client.ListenResponse()
// 启动一个业务
client.SendMessage();
}
通过对TCP原理的学习以及socket编程的实践,对计算机网络通信有了更进一步的了解。分析了c++网络编程以及go网络编程相同点以及不同点对连个语言的网络编程了解深入恶。