TCP/IP network programming: P1->Understanding network programming and sockets

This series of articles is the study notes of "TCP/IP Network Programming----Yin Shengyu"


1. Understand network programming and sockets

concept

①Network programming: Write a program to enable two computers connected to the network to exchange data with each other.
②Socket (socket): When we don't need to consider the physical connection, we only need to consider how to write the transmission software. The operating system provides a software facility called sockets for network data transfer. Therefore, network programming is also called socket programming.


1.1 Build a phone socket

example description

----Sockets are roughly divided into two types: TCP sockets and UDP sockets. Among them, the TCP socket can be compared to a telephone, and the telephone also completes the voice data exchange through the fixed telephone network. Therefore, there is actually not much difference between the landline we are familiar with and the socket. The following uses the telephone to explain the creation and use of sockets.
----​The phone can be used to dial or answer at the same time, but for sockets, there is a difference between dialing and answering. Let's first discuss the socket creation process for answering, and then use the telephone to explain the socket creation.
----The functions involved here will be explained one after another in the following chapters, we just use them here first.


① Dialogue by calling the socket function (installing the telephone)

Q: What do I need to prepare to answer the phone?
Answer: Of course it is a telephone!

The telephone can only be installed with a telephone, and then we will prepare a telephone. The following functions create sockets that are equivalent to telephones:

#include<sys/socket.h>
int socket(int domain, int type, int protocol);
//成功时返回文件描述符,失败时返回-1。

After getting the phone ready, consider assigning a phone number so that others can reach you.


② Dialogue when calling the bind function (assigning a phone number)

Q: What is your phone number?
A: My phone number is 123-1234.

Just like assigning a phone number to a phone (although the phone number is not actually assigned to the phone), use the following function to assign address information (IP address and port number) to the created socket:

#include<sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addren);
//成功时返回0,失败时返回-1。

After calling the bind function to assign an address to the socket, all preparations for answering the phone are basically completed. Next you need to connect the phone line and wait for an incoming call.


③ Dialogue when calling the listen function (connecting the telephone line)

Q: Is it only necessary to connect the telephone line after the telephone has been set up?
A: Yes, just connect to receive calls.

Once the phone line is connected, the phone is turned into an answerable state, at which point other people can make a call requesting to be connected to the phone. Similarly, the socket needs to be converted into a state that can receive connections:

#include<sys/socket.h>
int listen(int sockfd, int backlog);
//成功时返回0,失败时返回-1。

After the telephone line is connected, if someone makes a call, it will ring, and the phone can only be answered by picking up the handset.


④ Dialogue when calling the accept function (picking up the microphone)

Q: The phone is ringing, what should I do?
Answer: Answer it!

Picking up the microphone means accepting the other party's connection request. The same is true for sockets. If someone requests a connection to complete data transmission, the following function needs to be called to accept it.

#include<sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//成功时返回文件描述符,失败时返回-1。

Summarize

The socket creation process for accepting requests in network programming can be organized as follows:
① Step 1: Call the socket function to create a socket.
②The second step: Call the bind function to assign the IP address and port number.
③ Step 3: Call the listen function to change to accept the request state.
④ Step 4: Call the accept function to accept the connection request.


1.2 Write "Hello world!" server side

The server (server) is a program that can accept connection requests. The following builds the server side to verify the function call process mentioned above. After receiving the connection request, the server side returns a "Hello world!" reply to the requester. We only focus on the socket and related function call process, it is not necessary to understand all examples, because we have not covered any actual programming.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char *argv[])
{
    
    
    int serv_sock; // server socket
    int clnt_sock; // client socket

    struct sockaddr_in serv_addr; // 主套接字,用于持续监听连接请求
    struct sockaddr_in clnt_addr; // 辅助套接字:用于与客户端相连接以传输数据
    socklen_t clnt_addr_size;

    char message[] = "Hello World!";

    if (argc != 2) // 两个参数:可执行文件名、端口号
    {
    
    
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0); // 创建了一个 TCP 套接字
    if (serv_sock == -1)
        error_handling("socket() error");

    memset(&serv_addr, 0, sizeof(serv_addr));      // 将 serv_addr 全部填充为 0,主要是为了将 serv_addr 的 sin_zero 成员设为 0
    serv_addr.sin_family = AF_INET;                // 选定 IPv4 地址族
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // htonl:将 long 类型数据从主机字节序转换为网络字节序; INADDR_ANY:32 位整型值表示的 IP 地址
    serv_addr.sin_port = htons(atoi(argv[1]));     // 此程序运行时应该在文件名后跟一个端口号作为参数,如 hello_server 3030

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) // 将套接字与服务器的 IP 地址和端口号相绑定
        error_handling("bind() error");

    if (listen(serv_sock, 5) == -1) // 将套接字转换为接听状态
        error_handling("listen() error");

    clnt_addr_size = sizeof(clnt_addr);
    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); // 接受一个连接请求,并将 clnt_sock 套接字与其相连接
    if (clnt_sock == -1)
        error_handling("accept() error");

    write(clnt_sock, message, sizeof(message)); // 向客户端发送信息。注意:clnt_sock 不是客户端的套接字,而是服务器上真正与客户端相连接的套接字
    close(clnt_sock);                           // 关闭与客户连接的套接字:断开了该连接
    close(serv_sock);                           // 关闭监听端口的套接字:不再接受任何请求
    return 0;
}

void error_handling(char *message)
{
    
    
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

First execute gcc hello_server.c -o hserver the compilation of the source file, and then execute ./hserver 9190and wait for the arrival of the client message.
insert image description here


1.3 Build a call socket

The socket created on the server side is also called a server-side socket or a listening socket. The creation process of a client socket used to request a connection is much simpler. In addition, the client program only has 调用socket函数创建套接字and 调用connect函数向服务器端发送连接请求these two steps, so the client is simpler than the server.

①Call the socket function to create a socket

Question: "What do I need to prepare for the phone call?"
Answer: "Of course it is a telephone!"

②Call the connect function to send a connection request to the server

#include<sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addren);
//成功时返回0,失败时返回-1。

Just wait for the response after sending the connection request.


1.4 Writing a "Hello world!" client

The client only needs to complete two things: ① call the socket function and connect function ② run together with the server to send and receive string data.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char *message);

int main(int argc, char *argv[])
{
    
    
    int sock;
    struct sockaddr_in serv_addr;
    char message[30];
    int str_len;

    if (argc != 3)
    {
    
    
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }
    //创建套接字,此时套接字并不马上分为服务端和客户端。如果紧接着调用 bind,listen 函数,将成为服务器套接字
    //如果调用 connect 函数,将成为客户端套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));
    //调用 connect 函数向服务器发送连接请求
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error!");

    str_len = read(sock, message, sizeof(message) - 1);
    if (str_len == -1)
        error_handling("read() error!");

    printf("Message from server : %s \n", message);
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    
    
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

First execute gcc hello_client.c -o hclient compiling the source file, then execute ./hclient 192.168.123.128 9190sending a message to the server, and finally receive the message sent back by the server: Hello World!.
insert image description here


Two, Linux-based file operations

background

For Linux, there is no difference between socket operation and file operation, so it is necessary to understand files in detail. In the Linux world, sockets are also considered a type of file, so file I/O related functions can naturally be used during network data transmission. Windows is different from Linux in that it distinguishes between sockets and files. Therefore, special data transfer-related functions need to be called in Windows.

file descriptor

Definition: An integer assigned by the system to a file or socket.
Understanding file descriptors:
----There is a service station near the school that can copy the required papers with just a phone call.
---- There is a regular customer at the service station named Yingxiu, who always asks to copy part of the same paper. For example, "Anthropological Research on the Characteristics of Human Quality of Life-related Issues such as Touch, Perception, Thinking, Personality, and Intelligence, which are Gradually Elevated with the Highly Informationized Society", from pages 26 to 30 of the paper.
----This student makes several phone calls like this every day, and the speaking speed is very slow, which is a waste of time.
----So the uncle at the service station compiled this paper as No. 18, and Xiuying can directly say to help me copy pages 26 to 30 of No. 18 paper.
----Afterwards, the uncle will assign non-repeating numbers to other papers with long titles, which improves the efficiency a lot.
Roles:
① Uncle: Operating system
② Yingxiu: Programmer
③ Paper number: file descriptor
④ Paper: file or socket.
Summary: That is, whenever a file or socket is created, the OS will return the integer assigned to them. This integer will be the channel of good communication between the programmer and the operating system. In fact, a file descriptor is nothing more than a number given to a file or socket created by the operating system for convenience.

In addition, the standard input and output and standard error used in the process of learning C language are also allocated as the following file descriptors in Linux.

file descriptor object
0 Standard input: Standard Input
1 Standard output: Standard Output
2 Standard Error: Standard Error

Files and sockets are usually assigned file descriptors through the creation process. File descriptors are also sometimes called file handles, but handle is primarily a Windows term.


2.1 Write data to file

Opening Files
First, the functions that open files for reading and writing data are described. Two parameters need to be passed when calling this function: the first parameter is the name and path information of the target file to be opened, and the second parameter is the file opening mode (file characteristic information).

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int flag);
/*
成功时返回文件描述符,失败时返回-1
path : 文件名的字符串地址
flag : 文件打开模式信息
*/

The following table is the possible constant value and meaning of the second parameter flag of this function. If multiple parameters need to be passed, they should be combined and passed by the bitwise OR operation (OR) operator.

open mode meaning
O_CREATE Create files if necessary
O_TRUNC delete all existing data
O_APPEND Maintain existing data and save it later
O_RDONLY read only open
O_WRONLY write only open
O_RDWR read-write open

Close the file
After using the file, it must be closed. The function called when closing the file is introduced below:

#include<unistd.h>
int close(int fd);
/*
成功时返回0,失败时返回-1
参数:fd,需要关闭的文件或套接字的文件描述符。
*/

This function can close not only files, but also sockets. This once again proves that the Linux operating system does not distinguish between files and sockets.


Write data to a file
The write function described earlier is used to output (transfer) data to a file. Of course, there is no distinction between files and sockets in Linux. Therefore, this function is also used when passing data to other computers through sockets. The previous example also called it passing the string "Hello World!".

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
/*
成功时返回写入的字节数 ,失败时返回 -1
fd : 显示数据传输对象的文件描述符
buf : 保存要传输数据的缓冲值地址
nbytes : 要传输数据的字节数
*/

In this function definition, size_t is an unsigned int type declared by typedef. For ssize_t, the extra s in front of size_t means signed, that is, ssize_t is a signed int type declared by typedef.


Data types suffixed with _t

Problem: We have come into contact with unfamiliar data types such as ssize_t, , and so on. size_tThese are metadata types (primitive), which are generally defined by typedef declarations in the sys/types.h header file, which is an alias for the basic data types that everyone is familiar with. Since there are already basic data types, why declare and use these new ones?
Answer: It is generally believed that int is 32-bit, because mainstream operating systems and computers still use 32-bit. In the past 16-bit operating system era, the int type was 16-bit. According to the different systems and the changes of the times, the expression form of the data type also changes accordingly, and the data type used in the program needs to be modified. If size_t or ssize_t has been used before where the 4-byte data type needs to be declared, the code changes will be greatly reduced, because only the typedef declarations of size_t and ssize_t need to be modified and compiled. In a project, in order to give aliases to basic data types, a large number of typedef declarations are generally added. In order to distinguish from the new data types defined by the programmer, the data types defined by the operating system will add the suffix _t.


Example: Create a new file data.txt and save the data

#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
void error_handling(char * message);

int main(void)
{
    
    
	int fd;
	char buf[] = "Let's go!\n";
	fd = open("data.txt", O_CREAT| O_WRONLY| O_TRUNC);	//文件打开模式为O_CREAT、 O_WRONLY、 O_TRUNC的组合,因此
														//将创建空文件,并只能写。若存在data.txt文件,则清空文件的全部数据。
	if(fd == -1)
		error_handling("open() error!");
	printf("file descriptor: %d \n", fd);
	
	if(write(fd, buf, sizeof(buf)) == -1)	// 相对应于fd中保存的文件描述符的文件传输buf中保存的数据。
		error_handling("write() error!");
	close(fd);
	return 0;
}
void error_handling(char *message)
{
    
    
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

Execute and gcc low_open.c -o lopencompile the source file, execute ./lopenand run the program. You can see that a data.txt file has been added, and the content in it is exactly what we wrote in the code Let's go!
insert image description here


2.2 Read the data in the file

Read the data in the file
Corresponding to the previous write function, the read function is used to input (receive) data.

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
/*
成功时返回接收的字节数(但遇到文件结尾则返回 0),失败时返回 -1
fd : 显示数据接收对象的文件描述符
buf : 要保存接收的数据的缓冲地址值。
nbytes : 要接收数据的最大字节数
*/

Example: Read the contents of the data.txt file through the read function

#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#define BUF_SIZE 100
void error_handling(char * message);

int main(void)
{
    
    
	int fd;
	char buf[BUF_SIZE];

	fd = open("data.txt", O_RDONLY);	// 打开读取专用文件data.txt
	if(fd == -1)
	{
    
    
		error_handling("open() error!");
	}
	printf("file descriptor: %d \n", fd);	

	if(read(fd, buf, sizeof(buf)) == -1)	// 调用read函数向第11行中声明的数组buf保存读入的数据。
		error_handling("read() error!");
	printf("file data: %s", buf);
	close(fd);
	return 0;
}

void error_handling(char * message)
{
    
    
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

Execute and gcc low_read.c -o lreadcompile the source file, execute ./lreadand run the program. You can see that the content in data.txt is read out.
insert image description here


2.3 File descriptors and sockets

The following will create a file and a socket at the same time, and compare the returned file descriptor values ​​​​with integer types.

nclude <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>

int main()
{
    
    
    int fd1, fd2, fd3;
    //创建一个文件和两个套接字
    fd1 = socket(PF_INET, SOCK_STREAM, 0);
    fd2 = open("test.dat", O_CREAT | O_WRONLY | O_TRUNC);
    fd3 = socket(PF_INET, SOCK_DGRAM, 0);
    //输出之前创建的文件描述符的整数值
    printf("file descriptor 1: %d\n", fd1);
    printf("file descriptor 2: %d\n", fd2);
    printf("file descriptor 3: %d\n", fd3);

    close(fd1);
    close(fd2);
    close(fd3);
    return 0;
}

Execute and gcc fd_seri.c -o fdscompile the source file, execute ./fdsand run the program. From the output file descriptor integer value, it can be seen that the descriptors are numbered from small to large starting from 3, because 0, 1, and 2 are descriptors allocated to standard IO.
insert image description here


3. Realization based on Windows platform

Reasons to learn Linux and Windows at the same time

① Windows sockets (hereinafter referred to as Winsock) are mostly designed with reference to BSD series UNIX sockets, so many places are similar to Linux sockets. Therefore, only a part of the network program content edited in the Linux environment needs to be changed to run on the Windows platform.
②Most projects are developed on the server side under the Linux series of operating systems, while most clients are developed on the Windows platform. Not only that, but sometimes applications need to switch between the two platforms. Therefore, in the process of learning socket programming, it is necessary to take into account the two platforms of Windows and Linux. In addition, the socket programming under these two platforms is very similar. If the similar parts are explained together, the learning efficiency will be greatly improved.


3.1 Setting up header files and libraries for Windows Sockets programming

To do socket programming on Windows, you need:

Link ws2_32.lib library . In VS, add the ws2_32.lib library through: Project -> Properties -> Configuration Properties -> Linker -> Input -> Additional Dependencies.
insert image description here
②Import the header file WinSock2.h . There is a winsock.h and a WinSock2.h in Windows. Among them, WinSock2.h is a newer version, used to replace the former. In fact, on windows, you need to pass: Project->Properties->Configuration Properties->C++ to set the SDL check to No, otherwise there will be an error when running.
insert image description here


3.2 Initialization of Winsock

When performing Winsock programming, you must first call the WSAStartup function, set the Winsock version used in the program, and initialize the corresponding version of the library.

#include <WinSock2.h>
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData); // wVersionRequested:要用的 Winsock版本信息,lpWSAData:WSADATA 结构体变量的地址值
// 成功时返回 0,失败时返回非 0 的错误代码值

Parameter Description

WORD wVersionRequested
---- There are multiple versions in Winsock, the socket version information of the WORD type (WORD is the unsigned short type defined by the typedef declaration) should be prepared, and passed to the first parameter wVersionRequested of the function. If the version is 1.2, where 1 is the major version number and 2 is the minor version number, 0x0201 should be passed.
----As mentioned above, the upper 8 bits are the minor version number, and the lower 8 bits are the main version number, so as to transmit. This book mainly uses version 2.2, so 0x0202 should be passed. However, it is cumbersome to manually construct the version information in bytes. With the help of the MAKEWORD macro function, it is easy to construct WORD-type version information.
---- MAKEWORD(1,2);: The major version is 1, the minor version is 2, return 0x0201.
---- MAKEWORD(2,2);: The major version is 2, the minor version is 2, return 0x0202.
LPWSADATA lpWSAData
This parameter needs to pass in the address of the WSADATA structure variable (LPWSADATA is the pointer type of WSADATA). After the function is called, the corresponding parameters will be filled with the initialized library information. Although there is no special meaning, in order to call the function, the variable address of the WSADATA structure must be passed.


The WSAStartup function call process is given below, this code has almost become a Winsock programming formula.

int main(int argc, char* argv[])
{
    
    
    WSADATA wsaData;
    ...
    if(WSAStartup(MAKEWORD9(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error");
    ...
    return 0;    
}

The initialization method of the Winsock-related library has been introduced earlier, and then how to cancel the library-using the function given below.

#include <WinSock2.h>
int WSACleanup(void);  // 调用此函数,Winsock 相关库将还给操作系统,无法再调用 Winsock 相关函数。
                // 成功时返回 0,失败时返回 SOCKET_ERROR

4. Windows-based socket related functions and examples

4.1 Windows-based socket-related functions

The following are socket-related functions based on Windows. Although the return value and parameters are different from those of the Linux functions, the names of the functions with the same functions are the same. It is these features that make network programming across the two operating system platforms easier.

#include <WinSock2.h>

SOCKET socket(int af, int type, int protocol);                   // 成功时返回套接字句柄,失败时返回 INVALID_SOCKET

//与Linux的bind函数相同,调用其分配IP地址和端口号。
int bind(SOCKET s, const struct sockaddr* name, int namelen);    // 成功时返回 0,失败时返回 SOCKET_ERROR

//与Linux的listen函数相同,调用其使套接字可接收客户端连接。
int listen(SOCKET s, int backlog);                               // 成功时返回 0,失败时返回 SOCKET_ERROR

//与Linux的accept函数相同,调用其受理客户端连接请求。
SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);    // 成功时返回套接字句柄,失败时返回 INVALID_SOCKET

//与Linux的connect函数相同,调用其从客户端发送连接请求。
int connect(SOCKET s, const struct sockaddr* name, int namelen); // 成功时返回 0,失败时返回 SOCKET_ERROR

//这个函数在关闭套接字时调用。Linux中,关闭文件和套接字时都会调用close函数;而Windows中有专门用来关闭套接字的函数。
int closesocket(SOCKET s);                                       // 成功时返回 0,失败时返回 SOCKET_ERROR

4.2 File handles and socket handles in Windows

Sockets for Windows and Linux

----Linux also treats sockets as files internally. Therefore, regardless of creating a file or a socket, a file descriptor is returned. The process of returning and numbering file descriptors has also been introduced through examples.
----When a file is created by calling a system function in Windows, the handle is returned. In other words, a handle in Windows is equivalent to a file descriptor in Linux. It's just that Windows has to distinguish between file handles and socket handles. Although they are all called handles, they are not completely consistent like Linux. There is a difference between file handle related functions and socket handle related functions, which is different from Linux file descriptors.

Windows socket handle

Observe the socket-related functions based on Windows, which will deepen your understanding of the parameters and return values ​​of the SOCKET type. This is the new data type for holding the integer value of the socket handle, which is defined by the typedef declaration. Looking back at socket-related functions such as socket, listen, and accept, you can better understand the similarity with socket-related functions in Linux.

reason for the difference

Some programmers may ask: Since Winsock is designed based on the BSD socket of UNIX and Linux series, why not copy it, but there are certain differences?
----Some people think that this is a deliberate effort by Microsoft to prevent UNIX and Linux servers from being directly ported to Windows.
---- From the perspective of network program portability, this is also understandable.
----But I have a different opinion. In essence, there are huge differences in the kernel structure of the two operating systems, and the code implementation styles that depend on the operating system are also different. Even Windows programmers name variables differently from Linux programmers. Considering all aspects, it is more natural to maintain this difference. Therefore, I personally think that the socket programming methods of Windows socket and BSD series are different in order to maintain this natural difference.


4.3 Create Windows-based server and client

Windows server code

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>

void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
    
    
    WSADATA wsaData;
    SOCKET hServSock, hClntSock;
    SOCKADDR_IN servAddr, clntAddr;

    int szClntAddr;
    char message[] = "Hello World!";

    if (argc != 2)  // 检查参数数量
    {
    
    
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)  // 初始化 Winsock 相关库
        ErrorHandling("WSAStartup() error!");

    hServSock = socket(PF_INET, SOCK_STREAM, 0);    // 创建套接字
    if (hServSock == INVALID_SOCKET)
        ErrorHandling("socket() error");

    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;                  // 设置协议族
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 设置 IP 地址
    servAddr.sin_port = htons(atoi(argv[1]));       // 设置端口号

    if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)    // 为套接字分配地址和端口
        ErrorHandling("bind() error");

    if (listen(hServSock, 5) == SOCKET_ERROR)       // 使套接字转换为可接收连接的状态
        ErrorHandling("listen() error");

    szClntAddr = sizeof(clntAddr);
    hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);   // 接受连接请求,函数返回客户端的套接字
    if (hClntSock == INVALID_SOCKET)
        ErrorHandling("accept() error");

    send(hClntSock, message, sizeof(message), 0);   // 向客户端发送信息
    closesocket(hClntSock);     // 关闭服务器端套接字
    closesocket(hServSock);     // 关闭客户端套接字
    WSACleanup();       // 注销 Winsock 相关库
    return 0;
}

void ErrorHandling(char* message)
{
    
    
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

Open cmd and execute hServerWin 9190the message waiting for the client.
insert image description here


Windows client code

#pragma execution_character_set("utf-8")

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
    
    
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	char message[30];
	int strLen;

	if (argc != 3)
	{
    
    
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
		ErrorHandling("socket() error");

	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]);   // 这里对书中代码进行了一些修改(源代码编译会报错,根据报错提示修改为当前代码)
	servAddr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("connect() error!");

	strLen = recv(hSocket, message, sizeof(message) - 1, 0);
	if (strLen == -1)
		ErrorHandling("read() error!");
	printf("Message from server: %s \n", message);

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void ErrorHandling(char* message)
{
    
    
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

Open another cmd and execute hClientWin 192.168.31.222 9190the connection to the server.
insert image description here


4.4 Windows-based I/O functions

Sockets in Linux are also files, so data transmission can be performed through the file I/O functions read and write. In Windows, file I/O functions and socket I/O functions are strictly distinguished.

Winsock data transfer functions include the following two:

#include <WinSock2.h>
int send(SOCKET s, const char* buf, int len, int flags); 
/*
成功时返回传输字节数,失败时返回SOCKET_ERROR
s:数据传输对象连接的套接字句柄值
buf:保存待传输数据的缓冲地址值
len:要传输的字节数
flags:传输数据时用到的多种选项信息,一般可以写0。
*/

int recv(SOCKET s, const char* buf, int len, int flags); 
/*
成功时返回接收的字节数(收到文件尾 EOF 时为 0),失败时返回 SOCKET_ERROR
s:数据接受对象连接的套接字句柄值
buf:保存接受数据的缓冲地址值
len:能够接收的最大字节数
flags:接收数据时用到的多种选项信息
*/

Note:

Compared with the write function of Linux, the send function of Windows only has the last flags parameter. A detailed description of this parameter will be given in the following chapters, before that, just pass 0, indicating that no option is set. But one thing to note is that the send function is not unique to Windows. There is the same function in Linux, which also comes from BSD sockets. It's just that we only use the read and write functions in the Linux-related examples for the time being, in order to emphasize that file IO and socket IO are the same in the Linux environment.

Guess you like

Origin blog.csdn.net/InnerPeaceHQ/article/details/126392727