Home >Backend Development >C#.Net Tutorial >socket programming principles
1 Introduction to the problem
The I/O command set of the UNIX system evolved from the commands in Maltics and early systems. Its mode is open-write-read-close (open-write-read-close). When a user process performs an I/O operation, it first calls "open" to obtain the right to use the specified file or device, and returns an integer called a file descriptor to describe what the user is doing on the opened file or device. The process of I/O operations. This user process then calls "read/write" multiple times to transfer data. When all transfer operations are completed, the user process closes the call to notify the operating system that it has completed using an object.
When the TCP/ip protocol is integrated into the UNIX kernel, it is equivalent to the introduction of a new type of I/O operation in the UNIX system. The interaction between UNIX user processes and network protocols is much more complex than the interaction between user processes and traditional I/O devices. First of all, two processes that perform network operations are on the same machine. How to establish the connection between them? Secondly, there are many network protocols. How to establish a universal mechanism to support multiple protocols? These are all problems that network application programming interfaces need to solve.
In UNIX systems, there are two types of network application programming interfaces: sockets of UNIX BSD and TLI of UNIX System V. Since Sun adopted the UNIX BSD operating system that supports TCP/IP, the application of TCP/IP has developed further. Its network application programming interface, socket, has been widely used in network software and has been used to this day. The introduction of microcomputer operating systems DOS and Windows systems has become a powerful tool for developing network application software. This chapter will discuss this issue in detail.
2 Basic Concepts of Socket Programming Before you start using socket programming, you must first establish the following concepts.
2.1 Inter-network process communication
The concept of process communication originally originated from stand-alone systems. Since each process runs within its own address range, in order to ensure that two communicating processes do not interfere with each other and work in harmony, the operating system provides corresponding facilities for process communication, such as pipes in UNIX BSD ), named pipe (named pipe) and soft interrupt signal (signal), UNIX system V message (message), shared storage area (shared memory) and semaphore (semaphore), etc., but they are limited to use within the local process. communication between. Internet process communication aims to solve the problem of mutual communication between different host processes (process communication on the same machine can be regarded as a special case). To this end, the first thing to solve is the problem of process identification between networks. On the same host, different processes can be uniquely identified by process numbers (PRocess IDs). However, in a network environment, the process number assigned independently by each host cannot uniquely identify the process. For example, host A assigns a process number 5, and process number 5 can also exist in host B. Therefore, the sentence "process number 5" is meaningless.
Secondly, the operating system supports many network protocols. Different protocols work in different ways and have different address formats. Therefore, inter-network process communication must also solve the problem of identifying multiple protocols.
In order to solve the above problems, the TCP/IP protocol introduces the following concepts.
Port
A communication port that can be named and addressed in the network is a resource that can be allocated by the operating system.
According to the description of the OSI seven-layer protocol, the biggest functional difference between the transport layer and the network layer is that the transport layer provides process communication capabilities. In this sense, the final address of network communication is not just the host address, but also some kind of identifier that can describe the process. To this end, the TCP/IP protocol proposes the concept of protocol port (port for short), which is used to identify the communication process.
A port is an abstract software structure (including some data structures and I/O buffers). After an application (i.e. process) establishes a connection (binding) with a certain port through a system call, the data transmitted by the transport layer to the port is received by the corresponding process, and the data sent by the corresponding process to the transport layer is output through the port. In the implementation of the TCP/IP protocol, the port operation is similar to a general I/O operation. The process obtains a port, which is equivalent to obtaining a local unique I/O file, which can be accessed using general read and write primitives. Of.
Similar to a file descriptor, each port has an integer identifier called a port number, which is used to distinguish different ports. Since the two protocols of the TCP/IP transport layer, TCP and UDP, are two completely independent software modules, their respective port numbers are also independent of each other. For example, TCP has a port number 255, and UDP can also have a port number 255. There is no conflict.
The allocation of port numbers is an important issue. There are two basic allocation methods: the first is called global allocation, which is a centralized control method in which a recognized central organization performs unified allocation according to user needs and publishes the results to the public. The second type is local allocation, also known as dynamic connection. That is, when the process needs to access the transport layer service, it applies to the local operating system. The operating system returns a local unique port number, and the process then connects itself to the port through appropriate system calls. No. linked (tied). The above two methods are combined in the allocation of TCP/IP port numbers. TCP/IP divides the port number into two parts, a small number of which are reserved ports and are allocated to the service process in a global manner. Therefore, each standard server has a globally recognized port (well-known port), and its port number is the same even if the server is on the same machine. The remaining ports are free ports and are allocated locally. Both TCP and UDP stipulate that only port numbers less than 256 can be reserved ports.
Address
In network communication, the two processes communicating are on different machines. In an interconnection network, two machines may be on different networks, and these networks are connected through network interconnection devices (gateways, bridges, routers, etc.). Therefore, three-level addressing is required:
1. A certain host can be connected to multiple networks and must specify a specific network address;
2. Each host on the network should have its own unique address;
3. Each Each process on a host should have a unique identifier on that host.
Usually a host address consists of a network ID and a host ID, which is represented by a 32-bit integer value in the TCP/IP protocol; both TCP and UDP use 16-bit port numbers to identify user processes.
Network Byte Order
Different computers store multi-byte values in different orders. Some machines store low-order bytes at the starting address (lowest bytes first), and some store high-order bytes (highest bytes first). save first). To ensure data accuracy, network byte order must be specified in the network protocol. The TCP/IP protocol uses high-cost formats of 16-bit integers and 32-bit integers, which are included in the protocol header file.
Connection
The communication link between two processes is called a connection. Connections present themselves as some buffers and a set of protocol mechanisms, and externally show higher reliability than no connections.
Semi-related
To sum up, a triplet can be used to uniquely identify a process in the network:
(protocol, local address, local port number)
Such a triplet is called a half Half-association, which specifies each half of the connection.
Full correlation
A complete inter-network process communication needs to be composed of two processes, and can only use the same high-level protocol. In other words, it is impossible for one end of the communication to use the TCP protocol and the other end to use the UDP protocol. Therefore, a complete inter-network communication requires a five-tuple to identify:
(protocol, local address, local port number, remote address, remote port number)
Such a five-tuple is called an association , that is, only semi-correlations with the same protocol can be combined into a suitable correlation, or a connection can be completely specified.
2.2 Service Mode
In the network hierarchical structure, each layer is strictly one-way dependent, and the division of labor and collaboration at each level are concentrated in the interface between phasors. "Service" is an abstract concept that describes the relationship between phasors, that is, a set of operations provided by each layer in the network to the layer immediately above. The lower layer is the service provider, and the upper layer is the user requesting the service. Services are expressed in primitives, such as system calls or library functions. System calls are service primitives provided by the operating system kernel to network applications or high-level protocols. The n layer in the network must always provide more complete services to the n+1 layer than the n-1 layer, otherwise the n layer will have no value in existence.
In OSI terminology, the network layer and the layers below are also called communication subnets. They only provide point-to-point communication and have no concept of programs or processes. The transport layer implements "end-to-end" communication and introduces the concept of inter-network process communication. It also needs to solve problems such as error control, flow control, data sorting (message sorting), connection management, etc., and provides different service methods for this purpose. :
Connection-oriented (virtual circuit) or connectionless
Connection-oriented service is an abstraction of the telephone system service model, that is, every complete data transmission must go through the process of establishing a connection, using the connection, and terminating the connection. During the data transmission process, each data packet does not carry the destination address, but uses a connection number (connect ID). In essence, a connection is a pipe, and the data sent and received are not only in the same order, but also have the same content. The TCP protocol provides connection-oriented virtual circuits.
The connectionless service is an abstraction of the postal system service. Each packet carries a complete destination address, and each packet is transmitted independently in the system. The connectionless service cannot guarantee the order of packets, does not recover and retransmit packet errors, and does not guarantee the reliability of transmission. The UDP protocol provides connectionless datagram services.
The types of these two services and examples of their applications are given below:
Service type
Service
Example
Connection-oriented
Reliable message flow
Reliable byte stream
Unreliable connection
File transfer FTP Registered letter
Network database query
Sequence
In network transmission, two consecutive messages may go through different paths in end-to-end communication, so the order when they arrive at the destination may be different from when they are sent. "Sequence" means that the order of receiving data is the same as the order of sending data. The TCP protocol provides this service.
Error Control
A mechanism to ensure that the data received by the application is error-free. The method of checking errors is generally the "checksum" method. The way to ensure error-free transmission is for both parties to use confirmation response technology. The TCP protocol provides this service.
Flow control
A mechanism to control the data transmission rate during data transmission to ensure that data is not lost. The TCP protocol provides this service.
Byte stream
The byte stream method refers to only treating the transmitted message as a byte sequence and does not provide any boundaries for the data stream. The TCP protocol provides byte stream services.
Message
The receiver needs to save the sender’s message boundaries. The UDP protocol provides message services.
Full duplex/half duplex
End-to-end data is transmitted in two directions/one direction at the same time.
Cache/Out-of-Band Data
In the byte stream service, since there are no message boundaries, the user process can read or write any number of bytes at a certain time. To ensure correct transmission or when using a protocol with flow control, caching is required. But some extraordinary needs, such as interactive applications, may require this caching to be cancelled.
In the process of data transmission, a certain type of information that is not expected to be transmitted to the user through conventional transmission methods for timely processing, such as the interrupt key (Delete or Control-c) of the UNIX system, the terminal flow control characters (Control-s and Control -q), called out-of-band data. Logically, it seems that the user process uses an independent channel to transmit this data. This channel is associated with each pair of connected streams. Since the implementation of out-of-band data in Berkeley Software Distribution is inconsistent with the Host Agreement specified in RFC 1122, in order to minimize problems in interoperability, application writers must not require out-of-band data when interoperating with existing services. It's better not to use it.
2.3 Client/Server Model
In TCP/IP network applications, the main mode of interaction between the two communication processes is the Client/Server model, that is, the client sends a service request to the server, and the server receives Upon request, the corresponding service is provided. The establishment of the client/server model is based on the following two points: First, the reason for establishing a network is that the software and hardware resources, computing power and information in the network are unequal and need to be shared, thus creating hosts with many resources to provide services, and customers with fewer resources to request services. This non-reciprocal effect. Secondly, process communication on the Internet is completely asynchronous. There is neither a parent-child relationship nor a shared memory buffer between the processes communicating with each other. Therefore, a mechanism is needed to establish connections between processes that wish to communicate and provide data exchange between the two. Synchronization, this is TCP/IP based on client/server mode.
The client/server mode key operation process adopts an active request method:
First the server must be started and provide corresponding services according to the request:
1. Open a communication channel and notify the local host , it is willing to receive customer requests at a certain recognized address (common port, such as FTP is 21);
2. Wait for the customer request to arrive at this port;
3. Receive a repeated service request, process the request and send a response signal . When a concurrent service request is received, a new process must be activated to handle the customer request (such as fork and exec in UNIX systems). The new process handles this client request and does not need to respond to other requests. After the service is completed, the communication link between this new process and the client is closed and terminated.
4. Return to the second step and wait for another customer request.
5. Close the server
Client side:
1. Open a communication channel and connect to the specific port of the host where the server is located;
2. Send a service request message to the server, wait for and receive the response; continue to submit Request...
3. After the request is completed, the communication channel is closed and terminated.
It can be seen from the process described above:
1. The roles of the client and server processes are asymmetric, so the encoding is different.
2. The service process is generally started first in response to a user request. This service process exists as long as the system is running until it is terminated normally or forcefully.
2.4 Socket Type
TCP/IP socket provides the following three types of sockets.
Streaming Socket (SOCK_STREAM)
Provides a connection-oriented, reliable data transmission service. Data is sent without errors and duplication, and is received in the order in which it was sent. Built-in flow control prevents data flow from exceeding the limit; data is regarded as a byte stream with no length limit. File Transfer Protocol (FTP) uses streaming sockets.
Datagram socket (SOCK_DGRAM)
provides a connectionless service. Data packets are sent in the form of independent packets, no error-free guarantee is provided,
data may be lost or duplicated, and received out of order. Network File System (NFS) uses datagram sockets.
Raw socket (SOCK_RAW)
This interface allows direct access to lower layer protocols, such as IP and ICMP. Commonly used to verify new protocol implementations or access new devices configured in existing services.
3 Basic Socket System Calls
In order to better explain the principles of socket programming, several basic socket system call instructions are given below.
3.1 Create a socket──socket()
Before using a socket, the application must first have a socket. The system calls socket() to provide the application with the means to create a socket. The calling format is as follows :
SOCKET PASCAL FAR socket(int af, int type, int protocol);
This call needs to receive three parameters: af, type, protocol. The parameter af specifies the area where communication occurs. The address families supported by UNIX systems are: AF_UNIX, AF_INET, AF_NS, etc., while DOS and WINDOWS only support AF_INET, which is the Internet area. Therefore, the address family is the same as the protocol family. The type parameter describes the type of socket to be established. The protocol parameter indicates the specific protocol used by the socket. If the caller does not want to specify the protocol used, it is set to 0 and the default connection mode is used. Establish a socket based on these three parameters, allocate the corresponding resources to it, and return an integer socket number. Therefore, the socket() system call actually specifies the "protocol" element in the relevant quintuple.
For detailed description of socket(), please refer to 5.2.23.
3.2 Specify local address──bind()
When a socket is created using socket(), there is a name space (address family), but it is not named. bind() associates the socket address (including the local host address and the local port address) with the created socket number, that is, giving the name to the socket to specify local semi-relevance. The calling format is as follows:
int PASCAL FAR bind(SOCKET s, const strUCt sockaddr FAR * name, int namelen);
The parameter s is the socket descriptor (socket) returned by the socket() call and not connected. number). The parameter name is the local address (name) assigned to the socket s. Its length is variable, and its structure varies with the communication domain. namelen indicates the length of name.
If no error occurs, bind() returns 0. Otherwise the return value SOCKET_ERROR is returned.
Addresses play an important role in establishing socket communication. As a network application designer, you must be clearly familiar with the socket address structure. For example, UNIX BSD has a set of data structures that describe socket addresses. The address structure using the TCP/IP protocol is:
struct sockaddr_in{
short sin_family; /*AF_INET*/
u_short sin_port; /*16 bits Port number, network byte order*/
struct in_addr sin_addr; /*32-bit IP address, network byte order*/
char sin_zero[8]; /*reserved*/
}
About bind() For detailed description, please refer to 5.2.2.
3.3 Establishing a socket connection──connect() and accept()
These two system calls are used to complete a complete related establishment, among which connect() is used to establish a connection. A connectionless socket process can also call connect(), but at this time there is no actual message exchange between the processes, and the call will return directly from the local operating system. The advantage of this is that the programmer does not have to specify the destination address for each data, and if a datagram is received whose destination port has not established a "connection" with any socket, it can be determined that the port is reliable. operate. Accept() is used to make the server wait for the actual connection from a client process. The calling format of
connect() is as follows:
int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
The parameter s is the local socket descriptor to establish a connection. The parameter name points to a pointer describing the address structure of the other party's socket. The length of the other party's socket address is specified by namelen.
If no error occurs, connect() returns 0. Otherwise the return value SOCKET_ERROR is returned. In a connection-oriented protocol, this call results in the actual establishment of a connection between the local system and the external system.
Because the address family is always included in the first two bytes of the socket address structure and is related to a certain protocol family through the socket() call. Therefore bind() and connect() do not require a protocol as a parameter.
For detailed description of connect(), please refer to 5.2.4. The calling format of
accept() is as follows:
SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
The parameter s is the local socket descriptor, which is used as accept( ) call listen() should be called first. addr Pointer to the client's socket address structure, used to receive the address of the connected entity. The exact format of addr is determined by the address family established when the socket was created. addrlen is the length (number of bytes) of the client's socket address. If no error occurs, accept() returns a value of type SOCKET representing the descriptor of the received socket. Otherwise the return value INVALID_SOCKET.
accept() is used for connection-oriented servers. The parameters addr and addrlen store the client's address information. Before the call, the parameter addr points to an address structure with an initial value of empty, and the initial value of addrlen is 0; after calling accept(), the server waits to accept the client connection request from the socket numbered s, and the connection request is made by Issued by the client's connect() call. When a connection request arrives, the accept() call puts the address and length of the first client socket on the request connection queue into addr and addrlen, and creates a new socket number with the same characteristics as s. The new socket can be used to handle concurrent requests to the server.
For a detailed description of accept(), please refer to 5.2.1.
Four socket system calls, socket(), bind(), connect(), and accept(), can complete the establishment of a complete five-element correlation. socket () specifies the protocol element in the five-tuple, and its usage has nothing to do with whether it is a client or server, and whether it is connection-oriented. bind() specifies the local binary in the five-tuple, that is, the local host address and port number. Its usage is related to whether it is connection-oriented or not: on the server side, bind() must be called regardless of whether it is connection-oriented or not; key discipline discipline On the user side, if connection-oriented is adopted, bind() may not be called, but can be completed automatically through connect(). If connectionless is used, the client must use bind() to obtain a unique address.
The above discussion is only for the client/server model. In fact, the use of sockets is very flexible. The only principle to be followed is that a complete correlation must be established before process communication.
3.4 Listening for connections──listen()
This call is used to connect the server to indicate that it is willing to accept connections. listen() needs to be called before accept(), and its calling format is as follows:
int PASCAL FAR listen(SOCKET s, int backlog);
Parameter s identifies a local socket number that has been established but not yet connected. The server is willing Receive requests from it. Backlog represents the maximum length of the request connection queue, which is used to limit the number of queued requests. The currently allowed maximum value is 5. If no error occurs, listen() returns 0. Otherwise it returns SOCKET_ERROR.
listen() can complete the necessary connections for sockets that have not called bind() during the call process, and establish a request connection queue with a length of backlog.
Calling listen() is the third of the four steps for the server to receive a connection request. It is called after calling socket() to allocate a stream socket and calling bind() to assign a name to s, and must be called before accept().
For detailed description of listen(), please refer to 5.2.13.
As mentioned in Section 2.3, in the client/server model, there are two types of services: repeated services and concurrent services. The accept() call provides great convenience for implementing concurrent services because it returns a new socket number. Its typical structure is:
int initsockid, newsockid;
if ((initsockid = socket(... .)) < 0)
error(“can't create socket”);
if (bind(initsockid,....) < 0)
error(“bind error”);
if (listen(initsockid, 5) < 0)
error(“listen error”);
for (; {
newsockid = accept(initsockid, ...) /* blocking*/
if (newsockid < ; 0)
error(“accept error“);
if (fork() == 0){ /* Child process*/
closesocket(initsockid);
do(newsockid); /* Process request* /
exit(0);
}
closesocket(newsockid); /* Parent process*/
}
The result of the execution of this program is that the newsockid is related to the client's socket. After the sub-process is started, the initsockid of the continuing main server is closed, and the new newsockid is used to communicate with the client. The main server's initsockid can continue to wait for new client connection requests. Because in preemptive multitasking systems such as Unix, multiple processes can be performed simultaneously under system scheduling. Therefore, using a concurrent server allows the server process to have multiple sub-processes connecting and communicating with different client programs at the same time. From the perspective of the client program, the server can concurrently handle requests from multiple clients at the same time, which is where the name concurrent server comes from.
A connection-oriented server can also be a duplicate server. Its structure is as follows:
int initsockid, newsockid;
if ((initsockid = socket(....))<0)
error(“can't create socket");
if (bind(initsockid,....)<0)
error("bind error");
if (listen(initsockid,5)<0)
error(" listen error");
for (; {
newsockid = accept(initsockid, ...) /* blocking*/
if (newsockid < 0)
error("accept error");
do (newsockid); /* Processing requests*/
closesocket(newsockid);
}
The repeating server can only establish a connection with one client program at a time, and its processing of multiple client programs is repeated in a loop Therefore, it is called a duplicate server. Concurrent servers and duplicate servers have their own advantages and disadvantages: concurrent servers can improve the response speed of client programs, but it increases the overhead of system scheduling; duplicate servers are just the opposite, so users have to decide whether to use concurrent servers or not. When duplicating the server, it should be determined according to the actual situation of the application.
3.5 Data transmission──send() and recv()
After a connection is established, the commonly used system calls are send. () and recv(). The
send() call is used to send output data on the connected datagram or stream socket specified by key number s. The format is as follows:
int PASCAL FAR send( SOCKET s, const char FAR *buf, int len, int flags);
The parameter s is the connected local socket descriptor, and buf is a pointer to the buffer containing the sent data, and its length is specified by len. Specify the transmission control method, such as whether to send out-of-band data. If no error occurs, send() returns the total number of bytes sent.
See 5.2.19 for the detailed description of send(). The recv() call is used to receive input data on the connected datagram or stream socket specified by key s. The format is as follows:
int PASCAL FAR recv(SOCKET s, char FAR *buf, int len , int flags);
Parameter s is the connected socket descriptor. buf is a pointer to a buffer that receives input data, the length of which is specified by len. flags specifies the transmission control method, such as whether to receive out-of-band data, etc. If no errors occur, recv() returns the total number of bytes received. If the connection is closed, 0 is returned. Otherwise it returns SOCKET_ERROR.
For a detailed description of recv(), please refer to 5.2.16.
3.6 Input/output multiplexing─select()
The select() call is used to detect the status of one or more sockets. For each socket, this call can request read, write, or error status information. The set of sockets requesting a given state is indicated by an fd_set structure. On return, this structure is updated to reflect the subset of sockets that meet the specific condition. At the same time, the select() call returns the number of sockets that meet the condition. The call format is as follows:
int PASCAL FAR select (int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);
The parameter nfds specifies the value range of the socket descriptor being checked. This variable is generally ignored.
The parameter readfds points to a pointer to the set of socket descriptors to be read and tested, and the caller hopes to read data from it. The parameter writefds is a pointer to the set of socket descriptors to be tested for write. exceptfds Pointer to the set of socket descriptors to detect errors. timeout points to the maximum time the select() function waits. If set to NULL, it is a blocking operation. select() returns the total number of prepared socket descriptors contained in the fd_set structure, or returns SOCKET_ERROR if an error occurs.
For detailed description of select(), please refer to 5.2.18.
3.7 Close the socket──closesocket()
closesocket() closes the socket s and releases the resources allocated to the socket; if s involves an open TCP connection, the connection is released. The calling format of closesocket() is as follows:
BOOL PASCAL FAR closesocket(SOCKET s);
Parameter s is the socket descriptor to be closed. If no error occurs, closesocket() returns 0. Otherwise the return value SOCKET_ERROR is returned.
For detailed description of closesocket(), please refer to 5.2.3.
2.4 Examples of typical socket calling processes
As mentioned earlier, the application of TCP/IP protocol generally adopts the client/server mode, so in actual applications, there must be two processes, client and server, and the server is started first, and its system call The timing diagram is as follows.
The socket system call of a connection-oriented protocol (such as TCP) is shown in Figure 2.1:
The server must first be started until it completes the accept() call and enters the waiting state before it can receive client requests. If the client has been started before, connect() will return an error code and the connection is unsuccessful.
Figure 2.1 Connection-oriented socket system call timing diagram
The socket call of the connectionless protocol is shown in Figure 2.2:
Figure 2.2 The socket call timing diagram of the connectionless protocol
Connectionless server It must also be started first, otherwise the customer request will not be transmitted to the service process. Connectionless clients do not call connect(). Therefore, before the data is sent, a complete correlation has not been established between the client and the server, but a semi-correlation has been established through socket() and bind(). When sending data, the sender needs to specify the receiver's socket number in addition to the local socket number, thus dynamically establishing full correlation during the data sending and receiving process.
Example
This example uses the connection protocol-oriented client/server model, and its process is shown in Figure 2.3:
Figure 2.3 Connection-oriented application flow chart
Server-side program:
/* File Name: streams.c */
#include
#include
#define TRUE 1
/* This program establishes a socket and then starts an infinite loop; whenever it receives a connection through the loop, then Print out a message. When the connection is disconnected or a termination message is received, the connection ends and the program receives a new connection. The format of the command line is: streams */
main(
{
int sock, length;
struct sockaddr_in server;
struct sockaddr tcpaddr;
int msgsock;
char buf[1024];
int rval, len;
/* Create socket*/
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("opening stream socket") ;
exit(1);
}
/* Use any port to name the socket*/
server.sin_family = AF_INET;
server.sin_port = INADDR_ANY;
if (bind(sock, ( struct sockaddr *)&server, sizeof(server)) < 0) {
perror(“binding stream socket”);
exit(1);
}
/* Find the specified port number and Print it out */
length = sizeof(server);
if (getsockname(sock, (struct sockaddr *)&server, &length) < 0) {
perror(“getting socket name”);
exit( 1);
}
printf(“socket port #%d
”, ntohs(server.sin_port));
/* Start receiving connections*/
listen(sock, 5);
len = sizeof(struct sockaddr);
do {
msgsock = accept(sock, (struct sockaddr *)&tcpaddr, (int *)&len);
if (msgsock == -1)
perror(“accept”) ;
else do{
memset(buf, 0, sizeof(buf));
if ((rval = recv(msgsock, buf, 1024)) < 0)
perror(“reading stream message”) ;
if (rval == 0)
printf(“ending connection
”);
else
printf(“-->%s
”, buf);
}while (rval != 0 );
closesocket(msgsock);
} while (TRUE);
/* Because this program already has an infinite loop, the socket "sock" is never explicitly closed. However, when the process is killed or terminates normally, all sockets are automatically closed. */
exit(0);
}
Client program:
/* File Name: streamc.c */
#include
#include
#define DATA “half a league, half a league...”
/* This program establishes a socket and then connects to the socket given on the command line; when the connection ends, sends
a message on the connection and then closes the socket. The format of the command line is: streamc host name port number
The port number must be the same as the port number of the server program*/
main(argc, argv)
int argc;
char *argv[ ];
{
int sock;
struct sockaddr_in server;
struct hostent *hp, *gethostbyname( ;
char buf[1024];
/* Create socket*/
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror(“opening stream socket”);
exit(1);
}
/* Connect the socket using the name specified on the command line*/
server.sin_family = AF_INET;
hp = gethostbyname(argv[1]);
if ( hp == 0) {
fprintf(stderr, “%s: unknown host
”, argv[1]);
exit(2);
}
memcpy((char*)&server.sin_addr, ( char*)hp->h_addr, hp->h_length);
sever.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&server, sizeof (server)) < 0) {
perror(“connecting stream socket”);
exit(3);
}
if (send(sock, DATA, sizeof(DATA)) < 0)
perror(“sending on stream socket”);
closesocket(sock);
exit(0);
}
2.5 A general example program
In the previous section, we introduced a Simple socket program example. From this example we can see that there is almost a pattern in using socket programming, that is, all programs call the same functions in the same order almost without exception. Therefore, we can imagine designing an intermediate layer that provides a few simple functions. The program only needs to call these functions to realize data transmission under the ordinary Internet test. Programmers do not need to care too much about socket programs. Design details.
In this section we will introduce a general network program interface, which provides several simple functions to the upper layer. Programmers can complete most network data transmission under the Internet test network by using these functions. These functions isolate socket programming from the upper layer. It uses connection-oriented streaming sockets and a non-blocking working mechanism. The program only needs to call these functions to query network messages and respond accordingly. These functions include:
l InitSocketsStruct: Initialize the socket structure and obtain the service port number. used by client programs.
l InitPassiveSock: Initialize the socket structure, obtain the service port number, and establish the main socket. Used by server programs.
l CloseMainSock: Close the main socket. Used by server programs.
l CreateConnection: Establish a connection. used by client programs.
l AcceptConnection: Accept connection. Used by server programs.
l CloseConnection: Close the connection.
l QuerySocketsMsg: Query socket messages.
l SendPacket: Send data.
l RecvPacket: Receive data.
2.5.1 Header file
/* File Name: tcpsock.h */
/* The header file includes system header files commonly used by socket programs (the header file given in this example is the header file under SCO Unix, The header files of other versions of Unix may be slightly different) and define two of our own data structures and their instance variables, as well as the function descriptions we provide. */
#include
#include
#include
#include
#include
#include
#include
#include lude
#include
#include
#include
#include
#include
typedef struct SocketsMsg{ /* Sockets message structure*/
int AcceptNum; /* Indicates whether there is an external connection waiting to be received*/
int ReadNum; / * The number of connections with external data waiting to be read */
int ReadQueue[32]; /* The connection queue with external data waiting to be read */
int WriteNum; /* The number of connections that can send data*/
int WriteQueue[32]; /* Connection queue that can send data*/
int ExceptNum; /* Number of connections with exceptions*/
int ExceptQueue[32]; /* Connection queue with exceptions*/
} SocketsMsg;
typedef struct Sockets { /* Socket structure*/
int DaemonSock; /* Main socket*/
int SockNum; /* Number of data sockets*/
int Sockets[64 ]; /* Data socket array*/
fd_set readfds, writefds, exceptfds; /* Set of readable, writable, and exception sockets to be detected*/
int Port; /* Port number* /
} Sockets;
Sockets Mysock; /* Global variables*/
SocketsMsg SockMsg;
int InitSocketsStruct(char * servicename) ;
int InitPassiveSock(char * servicename) ;
void CloseMainSock() ;
int CreateConnection(struct in_addr *sin_addr);
int AcceptConnection(struct in_addr *IPaddr);
int CloseConnection(int Sockno);
int QuerySocketsMsg();
int SendPacket(int Sockno, void *buf , int len);
int RecvPacket(int Sockno, void *buf, int size);
2.5.2 Function source file
/* File Name: tcpsock.c */
/* This file gives the source code of nine functions, with Chinese comments in some places*/
#include "tcpsock.h"
int InitSocketsStruct(char * servicename)
/* Initialize Sockets structure. If succeed then return 1, else return error code (<0) */
/* This function is used for client programs that only need active sockets. It is used to obtain service information. The definition of service
is in the /etc/services file*/
{
struct servent *servrec;
struct sockaddr_in serv_addr;
if ((servrec = getservbyname(servicename, "tcp")) == NULL ) {
return(-1);
}
bzero((char *)&Mysock, sizeof(Sockets));
Mysock.Port = servrec->s_port; /* Service Port in Network Byte Order * /
return(1);
}
int InitPassiveSock(char * servicename)
/* Initialize Passive Socket. If succeed then return 1, else return error code (<0) */
/* This function is used for server programs that require passive sockets. In addition to obtaining service information, it also establishes a passive socket. */
{
int mainsock, flag=1;
struct servent *servrec;
struct sockaddr_in serv_addr;
if ((servrec = getservbyname(servicename, "tcp")) == NULL) {
return(-1);
}
bzero((char *)&Mysock, sizeof(Sockets));
Mysock.Port = servrec->s_port; /* Service Port in Network Byte Order */
if((mainsock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
return(-2);
}
bzero((char *)&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* Any network interface*/
serv_addr.sin_port = servrec->s_port;
if (bind(mainsock, (struct sockaddr) *)&serv_addr, sizeof(serv_addr)) < 0) {
close(mainsock);
return(-3);
}
if (listen(mainsock, 5) == -1) { / * Turn the active socket into a passive socket, ready to receive connections*/
close(mainsock);
return(-4);
}
/* Set this socket as a Non-blocking socket. */
if (ioctl(mainsock, FIONBIO, &flag) == -1) {
close(mainsock);
return(-5);
}
Mysock.DaemonSock = mainsock;
FD_SET(mainsock, &Mysock.readfds); /* Declares interest in the main socket being "readable"*/
FD_SET(mainsock, &Mysock.exceptfds); /* Declares interest in exception events on the main socket */
return(1);
}
void CloseMainSock()
/* Close the main socket and clear the declaration of the events above it. It is a good practice to close the main socket before the end of the program*/
{
close(Mysock.DaemonSock);
FD_CLR(Mysock.DaemonSock, &Mysock.readfds);
FD_CLR(Mysock.DaemonSock, &Mysock. exceptfds);
}
int CreateConnection(struct in_addr *sin_addr)
/* Create a Connection to remote host which IP address is in sin_addr.
Param: sin_addr indicates the IP address in Network Byte Order.
if succeed return the socket number which indicates this connection,
else return error code (<0) */
{
struct sockaddr_in server; /* server address */
int tmpsock, flag=1, i;
if ((tmpsock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return(-1);
server.sin_family = AF_INET;
server.sin_port = Mysock.Port;
server.sin_addr.s_addr = sin_addr->s_addr;
/* Set this socket as a Non-blocking socket. */
if (ioctl(tmpsock, FIONBIO, &flag) == -1) {
close (tmpsock);
return(-2);
}
/* Connect to the server. */
if (connect(tmpsock, (struct sockaddr *)&server, sizeof(server)) < 0 ) {
if ((errno != EWOULDBLOCK) && (errno != EINPROGRESS)) {
/* If the error code is EWOULDBLOCK and EINPROGRESS, there is no need to close the socket, because the system will continue to open the socket later Establish a connection. Whether the connection is successfully established can be determined by using the select() function to detect whether the socket is "writable". */
close(tmpsock);
return(-3); /* Connect error. */
}
}
FD_SET(tmpsock, &Mysock.readfds); writefds);
FD_SET(tmpsock, &Mysock.exceptfds);
i = 0;
while (Mysock.Sockets[i] != 0) i++; /* look for a blank sockets position */
if (i >= 64) {
close(tmpsock);
return(-4); /* too many connections */
}
Mysock.Sockets[i] = tmpsock;
Mysock.SockNum++;
return(i);
}
int AcceptConnection(struct in_addr *IPaddr)
/* Accept a connection. If succeed, return the data sockets number, else return -1. */
{
int newsock, len, flag=1, i;
struct sockaddr_in addr;
len = sizeof(addr);
bzero((char *)&addr, len);
if ((newsock = accept(Mysock.DaemonSock, &addr, &len)) == -1)
return(-1); /* Accept error. */
/* Set this socket as a Non-blocking socket. */
ioctl(newsock, FIONBIO, &flag);
FD_SET(newsock, &Mysock.readfds);
FD_SET(newsock, &Mysock.writefds);
FD_SET(newsock, &Mysock.exceptfds);
/* Return IP address in the Parameter. */
IPaddr->s_addr = addr.sin_addr.s_addr;
i = 0;
while (Mysock.Sockets[i] != 0) i++; /* look for a blank sockets position */
if (i >= 64) {
close(newsock);
return(-4); /* too many connections */
}
Mysock.Sockets[i] = newsock;
Mysock.SockNum++;
return(i);
}
int CloseConnection(int Sockno)
/* Close a connection indicated by Sockno. */
{
int retcode;
if ((Sockno >= 64) (Sockno < 0) (Mysock.Sockets[Sockno] == 0))
return(0);
retcode = close(Mysock.Sockets[Sockno]);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.readfds);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.writefds);
FD_CLR(Mysock.Sockets[Sockno], &Mysock.exceptfds);
Mysock.Sockets[Sockno] = 0;
Mysock.SockNum--;
return(retcode);
}
int QuerySocketsMsg()
/* Query Sockets Message. If succeed return message number, else return -1.
The message information stored in struct SockMsg. */
{
fd_set rfds, wfds, efds;
int retcode, i;
struct timeval TimeOut;
rfds = Mysock.readfds;
wfds = Mysock.writefds;
efds = Mysock.exceptfds;
TimeOut.tv_sec = 0; /* 立即返回,不阻塞。*/
TimeOut.tv_usec = 0;
bzero((char *)&SockMsg, sizeof(SockMsg));
if ((retcode = select(64, &rfds, &wfds, &efds, &TimeOut)) == 0)
return(0);
if (FD_ISSET(Mysock.DaemonSock, &rfds))
SockMsg.AcceptNum = 1; /* some client call server. */
for (i=0; i<64; i++) /* Data in message */
{
if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &rfds)))
SockMsg.ReadQueue[SockMsg.ReadNum++] = i;
}
for (i=0; i<64; i++) /* Data out ready message */
{
if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &wfds)))
SockMsg.WriteQueue[SockMsg.WriteNum++] = i;
}
if (FD_ISSET(Mysock.DaemonSock, &efds))
SockMsg.AcceptNum = -1; /* server socket error. */
for (i=0; i<64; i++) /* Error message */
{
if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &efds)))
SockMsg.ExceptQueue[SockMsg.ExceptNum++] = i;
}
return(retcode);
}
int SendPacket(int Sockno, void *buf, int len)
/* Send a packet. If succeed return the number of send data, else return -1 */
{
int actlen;
if ((Sockno >= 64) (Sockno < 0) (Mysock.Sockets[Sockno] == 0))
return(0);
if ((actlen = send(Mysock.Sockets[Sockno], buf, len, 0)) < 0)
return(-1);
return(actlen);
}
int RecvPacket(int Sockno, void *buf, int size)
/* Receive a packet. If succeed return the number of receive data, else if the connection
is shutdown by peer then return 0, otherwise return 0-errno */
{
int actlen;
if ((Sockno >= 64) (Sockno < 0) (Mysock.Sockets[Sockno] == 0))
return(0);
if ((actlen = recv(Mysock.Sockets[Sockno], buf, size, 0)) < 0)
return(0-errno);
return(actlen); /* actlen is the received data length, if it is zero, it indicates The connection was closed by the other party. */
}
2.5.3 Simple server program example
/* File Name: server.c */
/* This is a very simple repetitive server program. After initializing the passive socket, it loops Waiting to receive a connection. If a connection is received, it displays the data socket serial number and the client's IP address; if data arrives on the data socket, it receives the data and displays the data socket serial number of the connection and the received string. */
#include "tcpsock.h"
main(argc, argv)
int argc;
char **argv;
{
struct in_addr sin_addr;
int retcode, i;
char buf[32];
/* For server programs, it is often in an infinite loop state. It only ends when the user actively kills the process or the system is shut down. For server programs that are forcibly terminated using kill, since the main socket is not closed and resources are not actively released, it may have an impact on subsequent server program restarts. Therefore, it is a good habit to actively close the main socket. The following statement causes the program to first execute the CloseMainSock() function to close the main socket when it receives signals such as SIGINT, SIGQUIT, and SIGTERM, and then ends the program. Therefore, when using kill to forcibly terminate the server process, you should first use kill -2 PID to give the server program a message to close the main socket, and then use kill -9 PID to forcibly end the process. */
(void) signal(SIGINT, CloseMainSock);
(void) signal(SIGQUIT, CloseMainSock);
(void) signal(SIGTERM, CloseMainSock);
if ((retcode = InitPassiveSock("TestService ")) < 0) {
printf("InitPassiveSock: error code = %d
", retcode);
exit(-1);
}
while (1) {
retcode = QuerySocketsMsg (); /* Query network messages*/
if (SockMsg.AcceptNum == 1) { /* Is there an external connection waiting to be received? */
retcode = AcceptConnection(&sin_addr);
printf("retcode = %d, IP = %s
", retcode, inet_ntoa(sin_addr.s_addr));
}
else if (SockMsg.AcceptNum = = -1) /* Primary socket error? */
printf("Daemon Sockets error.
");
for (i=0; i
if ((retcode = RecvPacket(SockMsg.ReadQueue[i], buf, 32)) > 0)
printf("sockno %d Recv string = %s
", SockMsg.ReadQueue[i], buf);
else /* The returned data length is zero, indicating that the connection is interrupted */
CloseConnection( SockMsg.ReadQueue[i]);
}
} /* end while */
}
2.5.4 Simple client program example
/* File Name: client.c */
/* The client program is in When executing, it first initializes the data structure and then waits for the user to enter a command. It recognizes four commands:
conn(ect): Establish a connection with the server;
send: Send data to the specified connection;
clos(e): Close Specify the connection;
quit: Exit the client program.
*/
#include "tcpsock.h"
main(argc, argv)
int argc;
char **argv;
{
char cmd_buf[16];
struct in_addr sin_addr;
int sockno1, retcode;
char *buf = "This is a string for test.";
sin_addr.s_addr = inet_addr("166.111.5.249") ; /* IP address of the host running the server program */
if ((retcode = InitSocketsStruct("TestService")) < 0) { /* Initialization data structure */
printf("InitSocketsStruct: error code = %d
", retcode);
exit(1);
}
while (1) {
printf(">");
gets(cmd_buf);
if (!strncmp (cmd_buf, "conn", 4)) {
retcode = CreateConnection(&sin_addr); /* Create connection*/
printf("return code: %d
", retcode);
}
else if( !strncmp(cmd_buf, "send", 4)) {
printf("Sockets Number:");
scanf("%d", &sockno1);
retcode = SendPacket(sockno1, buf, 26); / * Send data*/
printf("return code: %d
", retcode, sizeof(buf));
}
else if (!strncmp(cmd_buf, "close", 4)) {
printf("Sockets Number:");
scanf("%d", &sockno1);
retcode = CloseConnection(sockno1); /* Close the connection*/
printf("return code: %d
", retcode );
}
else if (!strncmp(cmd_buf, "quit", 4))
exit(0); Please follow the PHP Chinese website (www.php.cn) for articles