[Linux] Realization of udp client windows version and Tcp server

The windows version client is more suitable for most people~

Article Directory

  • 1. udp client windows version
  • 2. Implementation of Tcp server
  • Summarize


One, udp client windows version

First of all, we modify the code of the udp large chat room implemented in the previous article. Note that we only modify the server code and make the code modification very simple, because we are just an example of how to use windows to make a client.

Our server header file remains unchanged, and the hander method in the .cc file is simplified:

static void Usage(string proc)
{
    cout<<"Usage:\n\t"<<proc<<" local_port\n\n";
}
void handerMessage(int sockfd,string clientip,uint16_t clientport,string message)
{
    string response = message;
    response += "[server echo]: ";
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    bzero(&client, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_port = htons(clientport);
    client.sin_addr.s_addr = inet_addr(clientip.c_str());
    // 构建好结构体后,我们要把处理的数据发给谁呢?当然是客户端了,客户端给我们发数据我们再将处理后的数据发回给客户端
    sendto(sockfd, response.c_str(), response.size(), 0, (struct sockaddr *)&client, len);
}
// ./udpServer port
int main(int argc,char* argv[])
{
    if (argc!=2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<udpServer> usvr(new udpServer(handerMessage,port));
    usvr->InitServer();
    usvr->start();
    return 0;
}

Next, we begin to demonstrate how to write the code of the udp client in the windows environment:

First we need a library that includes header files and lib:

#include <iostream>
#include <WinSock2.h>
#include <string>
#pragma comment(lib,"ws2_32.lib")

Then we need to start the windows socket and initialize winsocket:

int main()
{
	WSAData wsd;
	//启动Winsock
	//进行Winsocket的初始化,windows初始化socket网络库,申请2.2的版本
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "WSAStartup Error =" << WSAGetLastError() << endl;
		return 0;
	}
	else
	{
		cout << "WSAStartup Success" << endl;
	}
}

startup is the startup interface, and the parameters inside mean: initialize the socket network library and apply for version 2.2. If the return value of the startup function is equal to 0, it means that the startup is successful, otherwise it fails and we will print it. Then just like on linux, just create a socket:

Of course, our client needs to know the ip and port number of the server, and users generally don’t know these things, so we need to allow users to connect to windows directly by starting, we can put the server’s ip and port number in a file, or define it directly:

 The ip and port number here are filled with your server, don't be like me -.-.

Then we create the socket:

SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
	if (sock == SOCKET_ERROR)
	{
		cout << "socket ERROR = " << WSAGetLastError() << endl;
		return 1;
	}
	else
	{
		cout << "socket success" << endl;
	}
	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());
	string line;
	while (true)
	{
		cout << "Please Enter# ";
		getline(cin, line);
		int n = sendto(sock, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server));
		if (n < 0)
		{
			cerr << "sendto error" << endl;
			break;
		}
		//接收服务器的数据
		char buffer[1024];
		struct sockaddr_in client;
		int len = sizeof(client);
		n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&client, &len);
		if (n >= 0)
		{
			buffer[n] = 0;
		}
		cout << "[server echo]: " << buffer << endl;
	}

This is exactly the same as in linux, and finally we need to release all the resources related to the library:

//最后将使用库的相关资源全部释放掉 关闭套接字的文件描述符
	closesocket(sock);
	WSACleanup();
	return 0;
}
int main()
{
	WSAData wsd;
	//启动Winsock
	//进行Winsocket的初始化,windows初始化socket网络库,申请2.2的版本
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "WSAStartup Error =" << WSAGetLastError() << endl;
		return 0;
	}
	else
	{
		cout << "WSAStartup Success" << endl;
	}
	SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
	if (sock == SOCKET_ERROR)
	{
		cout << "socket ERROR = " << WSAGetLastError() << endl;
		return 1;
	}
	else
	{
		cout << "socket success" << endl;
	}
	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());
	string line;
	while (true)
	{
		cout << "Please Enter# ";
		getline(cin, line);
		int n = sendto(sock, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server));
		if (n < 0)
		{
			cerr << "sendto error" << endl;
			break;
		}
		//接收服务器的数据
		char buffer[1024];
		struct sockaddr_in client;
		int len = sizeof(client);
		n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&client, &len);
		if (n >= 0)
		{
			buffer[n] = 0;
		}
		cout << "[server echo]: " << buffer << endl;
	}

	//最后将使用库的相关资源全部释放掉 关闭套接字的文件描述符
	closesocket(sock);
	WSACleanup();
	return 0;
}

So we can find that the difference between the windows client and the linux client is that windows needs to start winsocket and initialize the network library first, and finally need to manually release the related resources of the library and close the file descriptor of the socket.

We found that the function inet_addr will report an error when it is running. The reason is that the vs compiler thinks this function is not safe. You can disable this error report or use the function recommended by vs:

 Note: 4996 represents the error message I just mentioned. Don’t understand that any error report can be banned. What you want to ban depends on the number of your error report.

Second, the implementation of the Tcp server

First of all, the implementation of tcp server must be more difficult than udp, but because udp is datagram-oriented, it is not as widely used in daily life as tcp, so we must master tcp. As before, we first create the header files that need to be used, such as tcpserver.hpp, tcpserver.cc.

Next, we first write out the framework of the server:

namespace server
{
    static const uint16_t gport = 8080;
    class TcpServer
    {

    public:
       TcpServer(const uint16_t& port = gport)
          :_port(port)
          ,_sock(-1)
       {

       }
       void initServer()
       {
          
       }
       void start()
       {
           
       }
       ~TcpServer()
       {

       }
    private: 
       int _sock;    
       uint16_t _port;
    };
}

 When we bound udp to ip and port number, we said that in fact, a server only needs port number to start, because we will bind any ip when binding IP, so as long as the user knows our port number, he can access our server, so there is no ip in our private variable, only port and file descriptor. In the constructor, we directly give a default port number, so that when we start, we can set the port number we want to use or use the default one directly. Then we also write the server.cc file:

#include "TcpServer.hpp"
#include <memory>
using namespace server;
static void Usage(string proc)
{
    cout<<"\nUasge:\n\t"<<proc<<" port\n\n";
}
//./tcpserver port
int main(int argc,char* argv[])
{
    if (argc!=2)
    {
        Usage(argv[0]);
        exit(USE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<TcpServer> tsvr(new TcpServer(port));
    tsvr->initServer();
    tsvr->start();
    return 0;
}

This is still the same as the previous udp server. The only thing to explain is that we gave the default when the server was constructed. In fact, it can be run with one parameter. However, we will follow the startup method of ./tcpserver port for the demonstration later.

Then we write the framework of the client again:

namespace client
{
    class TcpClient
    {

    public:
      TcpClient(const string& serverip,const uint16_t& serverport)
        :_serverip(serverip)
        ,_serverport(serverport)
        ,_sock(-1)
      {

      }
      void initClient()
      {

      }
      void start()
      {
        
      }
      ~TcpClient()
      {

      }

    private:
      int _sock;
      string _serverip;
      uint16_t _serverport;
    };
}

We will not explain the same udp client, and write client.cc by the way:

#include "TcpClient.hpp"
#include <memory>
using namespace client;
static void Usage(string proc)
{
    cout<<"\nUsage:\n\t"<<proc<<" serverip serverport\n\n";
}
// ./tcpclient serverip serverport
int main(int argc,char* argv[])
{
    if (argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t serverport = atoi(argv[2]);
    string serverip = argv[1];
    unique_ptr<TcpClient> tcet(new TcpClient(serverip,serverport));
    tcet->initClient();
    tcet->start();
    return 0;
}

After the preparatory work is done, we start writing the initialization function of the server:

Our first step is to create a socket, but this time we can add a log function. Every time the server starts, it can tell us whether those function interfaces are called successfully, so we create another log.hpp:

#pragma once
#include <iostream>
#include <string>
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
void logMessage(int level,const std::string &message)
{
    //[日志等级][时间戳/时间][pid][message]
    std::cout<<message<<std::endl;
}

We divide the levels into 5, 0, 1, and 2 can be regarded as normal, 3, 4 means that some part is wrong or the function fails to run, and then we will simply print it first, and we will add interesting functions to the log after the Tcp server is implemented later.

  void initServer()
       {
           //1.创建文件套接字对象
           _sock = socket(AF_INET,SOCK_STREAM,0);
           if (_sock==-1)
           {
              logMessage(FATAL,"create socket error");
              exit(SOCKET_ERR);
           }
           logMessage(NORMAL,"socket success");
           //2.进行bind
           struct sockaddr_in local;
           bzero(&local,sizeof(local));
           local.sin_family = AF_INET;
           local.sin_port = htons(_port);
           local.sin_addr.s_addr = INADDR_ANY; //INADDR_ANY绑定任意地址IP
           if (bind(_sock,(struct sockaddr*)&local,sizeof(local))<0)
           {
              logMessage(FATAL,"bind socket error");
              exit(BIND_ERR);
           }
           logMessage(NORMAL,"bind socket success"); 
       }

The first two steps of tcp server initialization are exactly the same as udp, both create a socket first and then bind. First we create a socket, and then because tcp is byte-oriented, we choose sock_stream as the second parameter of socket. If the creation fails, then we will print information to the log. If the interface fails like this, the error level must be fatal, and then we can also write an enumeration to save all exit codes.

    enum
    {
        SOCKET_ERR = 2
        ,USE_ERR
        ,BIND_ERR
        ,LISTEN_ERR
    };

If the socket creation was successful, we print the creation success to the log. When binding, we can see that we bind IP with INADDR_ANY. This option means to bind any IP, and then we judge whether the binding is successful. If it fails, write information to the log and exit. Below we will explain the difference between Tcp server initialization and udp:

 //3.Tcp需要将套接字状态设为listen状态来一直监听(因为Tcp是面向字节流的)
           if (listen(_sock,gbacklog)<0)
           {
               logMessage(FATAL,"listen socket error");
               exit(LISTEN_ERR);
           }
           logMessage(NORMAL,"listen socket success");
           
       }

First of all, a Tcp server is link-oriented. When the client wants to initiate a request to the server normally, the client cannot directly send a message to the server, but needs to establish a link first, which means that our server must be ready to accept the link sent by the client at all times, so how to do it? We need to set the socket to listen state.

Let's take a look at the documentation of the listen interface first:

The first parameter is the file descriptor we return using the socket, and the second parameter is the underlying full link length + 1. Here we will not explain the second parameter in detail, and we will explain it in detail later when we talk about the principle of tcp. To use this parameter we first define a variable:

 This variable can be 5, 10, 20 and the like can not be too large. If the monitoring is successful, we will write the successful information to the log.

In this way, we have finished writing the interface for tcp server initialization. I don’t know if you have such questions, why doesn’t udp need to monitor? This is because udp does not require a link, and the data sent to us by the client is the data itself. Since tcp is link-oriented, the first step of tcp is not to send data but to establish a link (this is the three-way handshake of Tcp, which will be discussed in detail later when we talk about the principle).

Below we write the start interface:

For a server, once it is started, it must be an endless loop:

       void start()
       {
           for (;;)
           {
            
           }
       }

 So what should we do after we start it? In udp, we receive the message from the client, and then process the message and send it back to the client. For the tcp server, we just said that we need to establish a connection first, and the accept interface is needed to establish a connection:

Note: The recvfrom that received messages in the previous udp server cannot be used in tcp.

The first parameter is a file descriptor, and the last two parameters are output parameters. The call interface will automatically fill in the structure for us, and the information of the filled structure is the client's ip and port. The return value of accept is a file descriptor, let's explain it below:

First of all, the meaning of the last two parameters of accept and the last two parameters of recvfrom are exactly the same. They both help us fill in the client’s ip and port number. The most important thing is the first parameter. The meaning of this parameter is different, because the return value of accept is a file descriptor. What is the relationship between this file descriptor and the file descriptor returned by the socket we created before? When we used the listen interface, we said that setting the socket to the listening state can always monitor whether the client wants to find a request link for us, and the first parameter of accept is actually the listening socket, because we can only communicate with the client if we successfully listen to the client’s request link, so the socket returned by accept is the socket we actually use to communicate with the client, so we should change the private member variable sock created at the beginning to listensock, because this variable is only used to monitor new links.

 After changing the name, we can understand the relationship between these two sockets more easily.

       void start()
       {
           for (;;)
           {
              //4.server获取新链接  未来真正使用的是accept返回的文件描述符
              struct sockaddr_in peer;
              socklen_t len = sizeof(peer);
              //  sock是和client通信的fd
              int sock = accept(_listensock,(struct sockaddr*)&peer,&len);
              //accept失败也无所谓,继续让accept去获取新链接
              if (sock<0)
              {
                  logMessage(ERROR,"accept error,next");
                  continue;
              }
              logMessage(NORMAL,"accept a new link success");
              cout<<"sock: "<<sock<<endl;
              //5.用sock和客户端通信,面向字节流的,后续全部都是文件操作
              serviceIO(sock);
              //对于一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致文件描述符泄漏
              close(sock);
           }
       }

To use accept, you need to create a structure first, and then we can print the socket after getting the returned socket. Note: It doesn't matter even if we fail to accept, because listensock will continue to listen to new connections from the client, so we cannot exit if we fail to accept. When we successfully get the sock needed to communicate with the client, we have to consider communicating with the client. Here we write a function for the server to communicate with the client, and pass the sock:

       void serviceID(int sock)
       { 
           while (true)
           {
             
           }
       }

 First of all, we need to be able to read the messages sent by the client, so we directly use the read interface used in the previous file learning:

 This interface is very simple. The first parameter is which file descriptor we read from, the second parameter is which buffer to read, and the third parameter is the size of the buffer.

       void serviceID(int sock)
       {
           char buffer[1024];
           while (true)
           {
              ssize_t n = read(sock,buffer,sizeof(buffer)-1);
              if (n>0)
              {
                 //目前我们先把读到的数据当成字符串
                 buffer[n] = 0;
                 cout<<"recv message: "<<buffer<<endl;
              } 
           }
       }

We define a buffer, and if the reading is successful, add "receiving message:" in front and print out the message. Next, we simply process the data and transfer the data back to the client. Note: ours is just a string for demonstration purposes. In fact, the data here can be arbitrary, such as structured.

We have also used the write interface. The first parameter is the file descriptor to write to, the second parameter is the buffer of the written message, and the third parameter is the size of the buffer. 

       void serviceIO(int sock)
       {
           char buffer[1024];
           while (true)
           {
              ssize_t n = read(sock,buffer,sizeof(buffer)-1);
              if (n>0)
              {
                 //目前我们先把读到的数据当成字符串
                 buffer[n] = 0;
                 cout<<"recv message: "<<buffer<<endl;
                 //将消息转回客户端
                 string outbuffer = buffer;
                 outbuffer+="[serverecho]";
                 write(sock,outbuffer.c_str(),outbuffer.size());  //多路转接解释write返回值
              }
              else if(n==0)
              {
                 //n==0说明客户端退出了
                 logMessage(NORMAL,"client quit,server me to!");
                 break;
              }
           }
       }

The return value of read is the size of the read data. If it reads 0, it means that the end of the file has been read. Since the end of the file has been read, the client must have exited. This is similar to a pipeline. When the writing end stops writing and the file descriptor is closed, our reading end will read the data and return 0 to the end of the file, so reading 0 means that the client has exited. This is why we close the file descriptor directly after serviceIO in start, because serviceIO is an infinite loop. Once the loop exits, it means that the client has exited. Since the client exits, our server must of course close the file descriptor that communicates with the client. (Note: We must close the file socket that has been used, otherwise it will cause a file descriptor leak)

Let's write the code for the client:

Client initialization also needs to create a socket. We said in udp that the client must be bound, but the programmer does not need to explicitly bind, and it is the same in tcp. So does the tcp client need to listen? Of course not, the client is not a server, no one will connect to the client, so there is no need to monitor, so do you need accept? The answer is that it is not needed, because no one on the client side is going to link so it is not needed.

      void initClient()
      {
         //   1.创建套接字
         _sock = socket(AF_INET,SOCK_STREAM,0);
         if (_sock<0)
         {
            cout<<"socket error"<<endl;
            exit(2);
         }
         //  2.客户端要bind吗?必须要! 要程序员显式的bind吗?不需要
         //  3.客户端要listen吗?不需要,没人去连客户端所以不需要
         //  4.客户端要accept吗?不需要
         //  5.客户端要发起链接。
      }

That's right, our client initialization code is very simple, we only need to create a socket.

So what does the client need to do to start it? It's actually our 5th point, we're going to initiate the link.

Let's get to know the connect interface:

 The first parameter is the file descriptor, the second and third parameters are the structure we want to pass, and this structure is the ip and port of which server we want to establish a connection with. If you look carefully at the part circled in red, you will find that when we just initialized, we said that the client does not need to explicitly bind, and you can see that it will automatically bind us in the connect interface. If the binding is successful, it will return 0.

      void start()
      {
         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());
         //connet的时候操作系统会帮客户端bind   返回值等于0成功
         if (connect(_sock,(struct sockaddr*)&server,sizeof(server))!=0)
         {
             cerr<<"socket connect error"<<endl;
         }
         else 
         {
            //链接成功客户端要干什么?与服务端通信
         }
      }

We first fill in the structure. After filling the ip and port of the server, we can connect. If the connection fails, we will print an error. Only when it succeeds can we start communicating with the server. How to communicate? In fact, it is the same as after the server accepts, because tcp is byte-oriented, so our communication process is all file operations.

   else 
         {
            //链接成功客户端要干什么?与服务端通信
            string message;
            while (true)
            {
                cout<<"Enter# ";
                getline(cin,message);
                //将消息发送给服务端
                write(_sock,message.c_str(),message.size());

                //读取服务端给我们发送的消息
                char buffer[1024];
                int n = read(_sock,buffer,sizeof(buffer)-1);
                if (n>0)
                {
                    buffer[n] = 0;
                    cout<<"Server回显# "<<buffer<<endl;
                }
                else 
                {
                    break;
                }
            }
         }

Here is an infinite loop to send a message to the server. We directly use getline to save the message input in cin to string, and then write the message to the file descriptor. Next, we need to read the message sent by the server. Because the read interface needs a buffer, we set a buffer. If the return value of read is greater than 0, we put a \0 at the position where the number of bytes is read, and then print the message. Descriptor, we can close it directly with the destructor here.

      ~TcpClient()
      {
         if (_sock!=-1)
         {
            close(_sock);
         }
      }

Let's run it and demonstrate it:

 After running, we can use the netstat command to check the server information, n means to display the port number and other information in numbers, l means to monitor, tp means tcp, let's start the client:

 You can guess why the socket we created is number 4? First of all, we all know that the OS will open 3 file descriptors by default, which are 0, 1, and 2, so why don't we create 3? Doesn't it mean that the file descriptor corresponds to the subscript of the array? This is because we used number 3 to create the listensock socket in our server, so the socket we created by calling accept is socket number 4.

 When we use the client to connect to the server, we query all tcp and find that there are two servers:

 This is because we are currently communicating locally, so there are two links, client to server and server to client. Under normal circumstances, different computers are connected. In this case, there is only one link to view.

Of course, when we wrote the code, we actually left a question on purpose. If there are multiple clients sending messages like this:

 Why can only the client connected first can send messages?

 When the client we linked first exited, why all the messages were sent all at once? This is because when we wrote the code, we only targeted one client, and whoever came in would send and receive messages in an endless loop:

 Only when one client exits the other client will receive the message. In the next article, we will use multi-process, multi-thread, thread pool version of serviceIO, then this problem will not exist.


Summarize

The implementation of the tcp server is not much more than that of the udp server, so only the udp understands that the simple tcp server is still very easy to implement. The next article is the focus of our tcp server.

Guess you like

Origin blog.csdn.net/Sxy_wspsby/article/details/131430549