GCDAsyncSocket source code using demo code

socket

For those of us who have no experience in network programming, our understanding of this socket is limited to the tutorial CS model point-to-point chat, which basically means that the server creates a socket and calls bind, Listen, and accept, while the client creates a socket and calls connect. This is just so. In fact, this is the basic flow of socket programming, as we will see in GCDAsyncSocket.

Source structure

GCDAsyncSocket has one.h file and one.m file.

The main class

GCDAsyncSocketPreBuffer

This class acts as a temporary buffer for reading from the socket. This class can be expanded.

@interface GCDAsyncSocketPreBuffer : NSObject { uint8_t *preBuffer; Size_t preBufferSize; // Uint8_t *readPointer; // Uint8_t *writePointer; // The position of the contents to be written to the preBuffer.Copy the code

The main structure is shown below

The ensureCapacityForWrite method is used to ensure that the current buffer can hold the number of bytes read from the socket.

- (void)ensureCapacityForWrite:(size_t)numBytes{size_t availableSpace = [self availableSpace]; If (numBytes > availableSpace){// Additional size size_t additionalBytes = numBytes - availableSpace; // New total size size_t newPreBufferSize = preBufferSize + additionalBytes; // Uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); // readPointer offset (read size) size_t readPointerOffset = readpointer-prebuffer; Size_t writePointerOffset = writepointer-prebuffer; // writePointer offset (write size) size_t writePointerOffset = writepointer-prebuffer; PreBuffer = newPreBuffer; // Reassign the size preBufferSize = newPreBufferSize; ReadPointer = preBuffer + readPointerOffset; writePointer = preBuffer + writePointerOffset; }}Copy the code

GCDAsyncReadPacket

This class also stores data, and this class is the main class for reading operations, why have two buffers to store data. If we can read all the data directly into the packet’s buffer without resizing it first, then we do so. Otherwise we use the prebuffer. It means that if we can store all the data into GCDAsyncReadPacket’s buffer without changing the size of GCDAsyncReadPacket, then we will use GCDAsyncReadPacket for a long time, otherwise we will use GCDAsync SocketPreBuffer. The specific reason is in this section of notes, I do not understand, interested can see.

@interface GCDAsyncReadPacket : NSObject{ @public NSMutableData *buffer; // Save the read data NSUInteger startOffset; // Offset NSUInteger bytesDone; // The current read size NSUInteger maxLength; NSTimeInterval timeout; NSUInteger readLength; NSData *term; // Read the split symbol BOOL bufferOwner; NSUInteger originalBufferLength; long tag; }Copy the code

readLengthForTermWithPreBuffer:found:

This method reads the preBuffer based on the delimiter to see how many bytes are in the delimiter and the content before the delimiter. If the format of the message we interact with is as follows, consisting of head+term+data, when we want to read the data, we should first read the head according to term, then read the size of the data of the current message according to the size in the head, and read the type.

GCDAsyncSocket

This class is the main socket classes, including the creation of the socket, bind, listen, accept, and read the data. We will omit the parts of ipv6, UN and SSL certificates in our analysis. The main fields are listed below

@implementation GCDAsyncSocket { uint32_t flags; // Record the current socket state uint16_t config; __weak ID <GCDAsyncSocketDelegate> delegate; __weak id<GCDAsyncSocketDelegate> delegate; // proxy dispatch_queue_T delegateQueue; // proxy dispatch_queue_T delegateQueue; Int socket4FD; socket4FD; socket4FD; NSData * connectInterface4; NSData * connectInterface4; //ipv4 address dispatch_queue_t socketQueue; // Accept is in this queue, and reads from socket4FD are in this queue, which must be a serial queue. // When ipv4Socket is connected to the server, souce is triggered and accept is called to connect to dispatch_source_t. //socket4FD source NSMutableArray *readQueue; // Read an array of tasks. GCDAsyncReadPacket *currentRead; // The current read task GCDAsyncSocketPreBuffer *preBuffer; // Unsigned long socketFDBytesAvailable; // How much data can be read in the current socket}Copy the code

init

Initialize a GCDAsyncSocket instance, set up the proxy, set up the proxy queue and socketQueue, and other write initializations

Configuration

Set proxy, proxy queue,ipv4,ipv6 availability, etc

Accepting

The main methods of creating a server socket, setting the parameters of creating a socket, creating a socket,listen,accept, etc

Disconnecting

Method of disconnecting

Diagnostics

Some utility class methods, getters

reading

The key way to read data, there are two main ways to read data, one is read by delimiter, one is read by specified size, both of which are useful in our demo. Because our demo adopts the mode of head+term+data, it will first read according to the delimiter, read to head, then get the size of data, and then read according to the specified size.

writing

The method of writing data

Security/Class Utilities, etc

We need to mention that this method +(NSData *)CRLFData returns our delimiter, which is actually \r\n. We won’t have \r\n in our demo data, so using this method, if we might have \r\n in our data, the delimiter will have to be re-designed.

Basic Execution process

Procedure for creating a socket

TYHSocketManager -- initSocket gcdSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()]; TYHSocketManager -- accept [gcdSocket acceptOnPort:Kport error:&error]; // Create a GCDAsyncSocket object and set the proxy queue to the main queue. // Accept the connection at the specified port.Copy the code

AcceptOnInterface (NSString)inInterface port (Uint16_t)port error (NSError *)errPtr This method, here we omit ipv4 and some verification already warning code, we will find that this is actually we usually contact with the socket programming basic API call. This method creates a socket for the server in the serial socketQueue, and then sets a listening source to execute any readable tasks on the serial socketQueue.

acceptOnInterface

- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr{ NSString *interface = [inInterface copy]; __block BOOL result = NO; __block NSError *err = nil; Int (^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { int socketFD = socket(domain, SOCK_STREAM, 0); int status; Status = FCNTL (socketFD, F_SETFL, O_NONBLOCK); int reuseOn = 1; status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); status = listen(socketFD, 1024); return socketFD; }; dispatch_block_t block = ^{ @autoreleasepool { [readQueue removeAllObjects]; [writeQueue removeAllObjects]; NSMutableData *interface4 = nil; NSMutableData *interface6 = nil; [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; Socket4FD = createSocket(AF_INET, interface4); // Create a read source, which is triggered when the socket4FD is readable, to execute accept4Source = on the serial socketQueue dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); int socketFD = socket4FD; dispatch_source_t acceptSource = accept4Source; __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(accept4Source, ^{@autoreleasepool {// This place will trigger on the child thread, but is running on the serial queue __strong GCDAsyncSocket *strongSelf = weakSelf; unsigned long i = 0; unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); }}); dispatch_resume(accept4Source); / / marker for the start of the current server socket flags | = kSocketStarted; result = YES; }}; / / the main is to ensure that the operation of the block is run on our serial socketQueue if (dispatch_get_specific (IsOnSocketQueueOrTargetQueueKey)) block (); Return dispatch_sync(socketQueue, block); elsedispatch_sync (socketQueue, block); return result; }Copy the code

doAccept

Take a look at the doAccpet method, which also removes some ipv6 summing methods. This method calls the Accept method and retrieves childSocketFD, which is the socket used to interact with the client. Currently we are still executing in a socketQueue. Then asynchronously execute to the delegateQueue and create an instance of GCDAsyncSocket associated with childSocketFD. Then set the marker in order to start and connect. Then the acceptSocket socketQueue is asynchronously set to read source and write source.

- (BOOL)doAccept:(int)parentSocketFD{ int childSocketFD; struct sockaddr_in addr; socklen_t addrLen = sizeof(addr); childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); // Set it to non-blocking FCNTL (childSocketFD, F_SETFL, O_NONBLOCK); int nosigpipe = 1; setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); Delegate = delegate; delegate = delegate; delegate = delegate; delegate = delegate; delegate = delegate; delegate = delegate; delegate = delegate; delegate = delegate; // Dispatch_async (delegateQueue, delegateQueue, delegateQueue, delegateQueue) ^{@autoreleasepool {// This place returns to the agent queue (primary queue) // Create a serial queue of GCDAsyncSokcet based on the connected childSocketFD. But socketQueue is recreated GCDAsyncSocket * acceptedSocket = [[[self class] alloc] initWithDelegate: theDelegate delegateQueue:delegateQueue socketQueue:NULL]; acceptedSocket->socket4FD = childSocketFD; acceptedSocket->flags = (kSocketStarted | kConnected); // Setup read and write sources for accepted socket dispatch_async(acceptedSocket->socketQueue, ^{@autoreleasepool {// Another child thread,socketQueue [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; }}); If ([theDelegate respondsToSelector: @ the selector (socket: didAcceptNewSocket:)]) {/ / this sentence or in the home side columns [theDelegate socket: the self didAcceptNewSocket:acceptedSocket]; }}}); } return YES; }Copy the code

setupReadAndWriteSourcesForNewlyConnectedSocket

Set the readSource listener to be executed on the current socketQueue when a socketFD has readable data. Each new connection we make to childSocket is executed on a serial queue, which is the socketQueue associated with the server. Then each childFD creates a new GCDAsyncSocket instance associated with the socketQueue Create a serial socketQueue, so that each GCDAsyncSocket has its own serial socketQueue and the same global primary delegateQueue. Each childFD reads and writes are processed on its own serial queue. And set the current readable socketFDBytesAvailable to 0. If the client sends us data, we can process it on the socketQueue, calling the doReadData method. DoReadData let’s play it for a second.

- (void) setupReadAndWriteSourcesForNewlyConnectedSocket socketFD: (int) {/ / set the callback of read, write out readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { __strong GCDAsyncSocket *strongSelf = weakSelf; /** How much readable data is in the current readSource. This is a good place to look. Suppose my client sends very little data to the server. StrongSelf ->socketFDBytesAvailable = */ strongSelf->socketFDBytesAvailable = */ strongSelf->socketFDBytesAvailable = */ strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); StrongSelf ->socketFDBytesAvailable > 0 if (strongSelf->socketFDBytesAvailable > 0) EOF = End Of File [strongSelf doReadEOF]; }}); socketFDBytesAvailable = 0; flags &= ~kReadSourceSuspended; dispatch_resume(readSource); flags |= kSocketCanAcceptBytes; flags |= kWriteSourceSuspended; }Copy the code

socket:didAcceptNewSocket:

This place is executing on the delegateQueue main queue, there’s a new connection, there’s a connection, there’s no data sent yet. The readDataToData method is called to create an object to read data from, and then set the current read to be delimited by the delimiter (as our demo sends data by delimiter), as we’ll see later, moving on

- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{ //sock is the current server object, newSocket is the object connected to the server, we need to hold the newSocket object, or release it, add it to the array. Then call the readDataToData method [_Sockets addObject:newSocket]; The delimiter is set to [GCDAsyncSocket CRLFData] [newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110]; }Copy the code

readDataToData:withTimeout:buffer:bufferOffset:maxLength:tag:

At present, we create a GCDAsyncReadPacket object packet to read the packet. The size of the buffer we set is 0, but the terminator is set, we send it from above, because when we read the data, we need to read the header according to the delimiter first, and then read d according to the length Ata part. So the packet is read by separator

- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset maxLength:(NSUInteger)maxLength tag:(long)tag { GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer startOffset:offset maxLength:maxLength timeout:timeout readLength:0 terminator:data tag:tag]; Dispatch_async (socketQueue, ^{@autoreleasepool {// if (flags & kSocketStarted) &&! (flags & kForbidReadsWrites) {// Add a pack [readQueue addObject:packet] to the array of read data; // There might be a read task [self maybeDequeueRead]; }}}); }Copy the code

maybeDequeueRead

Only the key code is put here. When we get here, we have a new connection coming in, and our currentRead is nil, and the readQueue has put a readPack that reads by delimiter, so we take the currentRead “task” and assign it to the currentRead, and remove the read task

- (void)maybeDequeueRead{ // If we're not currently processing a read AND we have an available read stream if ((currentRead == nil) && (flags & kConnected)){ if ([readQueue count] > 0){ currentRead = [readQueue objectAtIndex:0]; [readQueue removeObjectAtIndex:0]; / / Setup read the timer (if men) / / set the timeout source [self setupReadTimerWithTimeout: currentRead - > timeout]; // Immediately read, if possible }}}Copy the code

Creating a socket summary

1. Create a server socket on the main thread that is not the main socketQueue and call the bind and listen functions to listen connections. 2. A read source is created, and the task is executed on the child thread but on the socketQueue. 3. If an acceptSocket connection is received by the child thread, the acceptSocket will be created in the main queue. 4. A read Source listener is created on the socketQueue of the acceptSocket to listen for clients to write data to the server. When you have some data, go back to the doReadData method. 5. Execute the proxy method on Primary delegateQueue. Create a pack for GCDAsyncReadPacket, a pack read by delimiter. 6. Call maybeDequeueRead and doReadData on the socketQueue of the acceptSocket.

Pay attention to

Each of our GCDAsyncSocket objects has its own socketQueue, including the Server’s GCDAsyncSocket object and the GCDAsyncSocket object created after the client connects, but they all use the same delegateQueue Each childSocket readSource is handled in its own socketQueue, independent of any other connection, and independent of the server’s socketQueue. In the preceding figure, steps 1-3 are performed in the socketQueue of Server Sokcet, and steps 6-8 are performed in the socketQueue connected to the server.

Read the data

doReadData

Let’s solve the above problem first. There are two parts of the call to doReadData, the agent after the connection is established and then the readPact is created and the listener is listening from the read source to the readable time. CurrentRead: socketFDBytesAvailable=0, socketFDBytesAvailable=0, socketFDBytesAvailable=0, socketFDBytesAvailable=0, socketFDBytesAvailable=0, socketFDBytesAvailable=0, socketFDBytesAvailable=0 AvailableBytes]==0, then return, and proceed if the read source has data coming later. 2. If we delay the agent in the main queue for some time, I sleep(1000), and the client sends the data to the server, then currentRead == nil will be read The source hangs, it doesn’t read any more data, it also returns, but socketFDBytesAvailable has a value, and when the main queue runs there, it goes down.

/ / when this currrentRead is empty, we know from the above, in the home side column callback will create the currentRead if ((currentRead = = nil) | | (flags & kReadsPaused)) {if (socketFDBytesAvailable > 0){ [self suspendReadSource]; } return; } BOOL hasBytesAvailable = NO; unsigned long estimatedBytesAvailable = 0; //socketFDBytesAvailable this value is assigned in the read source listener. estimatedBytesAvailable = socketFDBytesAvailable; hasBytesAvailable = (estimatedBytesAvailable > 0); if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)){ [self resumeReadSource]; return; }Copy the code

Three ways to read data

Looking at the comments we see that there are three ways to read data

  1. Read all readable data
  2. Read fixed-length data
  3. Reads data at the specified delimiter

We’re going to do 2,3 ways to read data in this demo

// There are 3 types of read packets:
// 
// 1) Read all available data.
// 2) Read a specific length of data.
// 3) Read up to a particular terminator.
Copy the code

Read process

Reading Process

The client sends the following binary data stream to the server. Term = "\r\n"; head = {"type": "TXT ", "size": "60"} term = "\r\n"; Data = {"GCDAsyncSocket source code read "}Copy the code
  1. If the readSource listener tells us that the current readable data size is 70, including head, term, and data
  2. PreBuffer allocates a 70-byte buffer preBuffer that contains a pointer to the beginning of the data space, a writePointer, and a readPointer, all of which point to the starting location.
  3. Read data from the socket and store data in the preBuffer of the preBuffer. If 70 bytes of data are stored in the preBuffer, move writePonter70 bytes to indicate that 70 bytes of data are written to the preBuffer.
  4. We iterate through the data in the preBuffer to find the position of term, and then read the data in the head part. Suppose head and term are 10 bytes. We store the head data, place it on an object, and prepare to return it.
  5. Then move readPointe10 bytes to indicate that we are currently reading 10 bytes, leaving the contents of writePointer-readpointer unread
  6. Once we get the head, we get 60 bytes of data from the head, and then we read the remaining 60 bytes of data from the preBuffer.
  7. After all data has been read, we reset the position of writePointer and readPointer.

read from socket

Read data into the preBuffer

The first time the client sends data to the server, there is no data in our preBuffe, so it will not read from the preBuffer. Will read directly from the socket. The reading process is as follows:

  1. CurrentRead ->term is not null and read as delimiter. The length of currentRead’s buffer is 0, so readIntoPreBuffer=true will be read into the temporary cache.
  2. [preBuffer ensureCapacityForWrite] [preBuffer ensureCapacityForWrite] [preBuffer ensureCapacityForWrite] [preBuffer ensureCapacityForWrite] [preBuffer ensureCapacityForWrite] [preBuffer ensureCapacityForWrite] [preBuffer ensureCapacityForWrite
  3. The writerPonter of the preBuffer is obtained and assigned to the buffer.
  4. To read the specified byte from socketFD into buffer, the read function is called
{ NSUInteger bytesToRead; if (currentRead->term ! = nil){ // Read type #3 - read up to a terminator bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable shouldPreBuffer:&readIntoPreBuffer]; }else{ bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; } if (bytesToRead > SIZE_MAX) { bytesToRead = SIZE_MAX; } if (readIntoPreBuffer){/* This method is used to ensure that the preBuffer can store as many bytes as bytesToRead. If the size of the preBuffer is only 1024, then the space of 1024 + (2048-1024) bytes will be reopened: And the various points to adjust preBuffer * / [preBuffer ensureCapacityForWrite: bytesToRead]; // This is the position of the writeBuffer pointer to the current preBuffer. Write current data from there = [preBuffer writeBuffer]; } else{ [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; } int socketFD = (socket4FD ! = SOCKET_NULL) ? socket4FD : (socket6FD ! = SOCKET_NULL) ? socket6FD : socketUN; ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); If (result < 0){if (errno == EWOULDBLOCK) waiting = YES; else error = [self errnoErrorWithReason:@"Error in read() function"]; socketFDBytesAvailable = 0; }else if (result == 0){EOF = End Of File socketEOF = YES; socketFDBytesAvailable = 0; }else{bytesRead = result; /* bytesRead: indicates the number of bytes actually read. BytesToRead: indicates the number of bytesRead. I want to read 10 bytes, only 8 bytes return, indicating packet loss. */ if (bytesRead < bytesToRead){ socketFDBytesAvailable = 0; }else{ if (socketFDBytesAvailable <= bytesRead) socketFDBytesAvailable = 0; else socketFDBytesAvailable -= bytesRead; } if (socketFDBytesAvailable == 0){ waiting = YES; }}}Copy the code

Copy data from preBuffer to currentRead

  1. Moved the write pointer to the preBuffer to indicate how much was written to it.
  2. ReadLengthForTermWithPreBuffer lookup delimiter, head size.
  3. Make sure currentRead has enough space, which will also be expanded
  4. Memcpy copies the header from the preBuffer to the curretRead buffer. At this point, the contents of the preBuffer have not been read.
  5. Move the read pointer to the preBuffer to indicate that head and term bytes have been read.
  6. Change bytesDone of currentRead to indicate how many bytes are currently read. Head is finished reading
  7. Call completeCurrentRead, and then call the proxy method in deletageQueue to return the read head.
  8. Then get to the size of the data from the head inside, and then according to the size of the specified byte to read, call methods readDataToLength: withTimeout: tag:
If (readIntoPreBuffer) {// Move the writeBuffer pointer to preBuffer. WriteBuffer reads bytesRead bytes from the socket and stores them in the writeBuffer location didWrite:bytesRead]; NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; // copy the contents of bytesToCopy to readBuf memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); PreBuffer didRead:bytesToCopy [preBuffer didRead:bytesToCopy]]; CurrentRead ->bytesDone += bytesToCopy; totalBytesReadForCurrentRead += bytesToCopy; }Copy the code

read from buffer

Now a GCDAsyncReadPacket is created, which is read according to the size of the data, because we already got the size of the data from head

  1. Reading from above, our availabelBytes preBuffer has value because we didn’t read it before, only the header was read
  2. EnsureCapacityForAdditionalDataOfLength to ensure there is enough space
  3. Memcpy copies data from preBuffer to currentRead
  4. Move the read pointer of the preBuffer. If readPointer==writePointer is found, readPointer and writePointer are reset.
  5. done = (currentRead->bytesDone == currentRead->readLength); The judgment function of this part is that if THE client sends 30M data to the server, the readSource may be triggered for several times. Only when the accumulated data read is the same as the size of the header, the readSource is regarded as finished.
if ([preBuffer availableBytes] > 0){ if (currentRead->term ! = nil){ bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; }else{ bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; } [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset+currentRead->bytesDone; // Read bytesToCopy bytes from preBuffer's readBuffer to buffer memcpy(buffer, [preBuffer readBuffer], bytesToCopy); PreBuffer didRead:bytesToCopy [preBuffer didRead:bytesToCopy] resets readPointer and writePointer if they are equal;  currentRead->bytesDone += bytesToCopy; totalBytesReadForCurrentRead += bytesToCopy; If (currentRead->readLength > 0){// bytesDone is equal to readLength done = (currentRead->bytesDone == currentRead->readLength); }else if (currentRead->term ! = nil){ if (! done && currentRead->maxLength > 0){ if (currentRead->bytesDone >= currentRead->maxLength){ error = [self readMaxedOutError]; } } } else{ done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); }}Copy the code

At this point, the analysis of data read by the server side of GCDAsyncSocket has been completed.

conclusion

  1. Through the reading of GCDAsyncSocket code, have a more specific understanding of socket programming.
  2. I know some routines of parsing buffer and have a further understanding of data flow.
  3. In fact, through this CS-demo, I have a deeper understanding of the process of three-way handshake, including sending two data and confirming only one packet. This phenomenon is encountered many times with the packet capture tool
  4. If THE client writes a large piece of data to the server, the process of subcontracting is handled well by TCP and IP layers. The buffer we receive is assembled. We need to agree on some reading strategies to enable us to correctly parse the data