This is a deep dive in to the source code for KCP. Despite the project claiming to have good documentation, I disagree. It’s simple, but it doesn’t do a great job explaining itself.

The following is a breakdown of the logic of each function.


The main source file.

ikcp_encode8u … ikcp_encode32u, ikcp_decode8u [private]

A bunch of functions for byte-swapping. the encode functions write to the pointer, while the decode functions read from it, writing to the input argument.

Available in 8, 16, and 32 bit unsigned type variants, for both encode and decode.

imin, imax, ibound [private]

A bunch of min/max code. Notable is the bound code which has an unusual behavior. It takes 3 arguments: lower, middle, upper. Step one is to _max_ the lower and middle bounds, then _min_ versus the upper bound.

Off the top of my head I’m not sure where this exact formula is useful, but we’ll see as we dive deeper.

_itimediff [private]

Simple time subtract function.

ikcp_malloc, ikcp_free [private]

If set, calls the ikcp_malloc_hook and ikcp_free_hook function pointers, otherwise calls standard malloc and free. Not a bad way to handle this IMO (especially for fallback reasons). That said we could save a cycle if we simply assigned malloc and free to the hooks directly in some init function, or set it via a #DEFINE.

ikcp_allocator [PUBLIC]

Assigns the malloc and free hooks.

ikcp_segment_new, ikcp_segment_delete [private]

Using the allocators, allocates an IKCPSEG structure with a bunch of data tagged on the end.

struct IKCPSEG {
	struct IQUEUEHEAD node;
	IUINT32 conv;
	IUINT32 cmd;
	IUINT32 frg;
	IUINT32 wnd;
	IUINT32 ts;
	IUINT32 sn;
	IUINT32 una;
	IUINT32 len;
	IUINT32 resendts;
	IUINT32 rto;
	IUINT32 fastack;
	IUINT32 xmit;
	char data[1];

ikcp_log [PUBLIC], ikcp_canlog [private]

An printf-like wrapper for internal logging. A writelog function must be assigned to the kcp structure for this to work. ikcp_canlog checks if appropriate flags and pointers are set for logging to work in the first place.

Logs can include a mask, which is compared against kcp->logmask.

#define IKCP_LOG_OUTPUT			1
#define IKCP_LOG_INPUT			2
#define IKCP_LOG_SEND			4
#define IKCP_LOG_RECV			8
#define IKCP_LOG_IN_DATA		16
#define IKCP_LOG_IN_ACK			32
#define IKCP_LOG_IN_PROBE		64
#define IKCP_LOG_IN_WINS		128
#define IKCP_LOG_OUT_DATA		256
#define IKCP_LOG_OUT_ACK		512
#define IKCP_LOG_OUT_PROBE		1024
#define IKCP_LOG_OUT_WINS		2048

The default logmask is 0, meaning all logging is disabled. Curiously there is no IKCP_LOG_ALL constant. The author probably hardcodes this value to -1.

ikcp_output, ikcp_setoutput [PUBLIC]

Okay, ikcp_output is in the helper function section, but it’s actually a key function.

All data that is sent by KCP goes through this function. You hook up a function by either setting kcp->output or calling ikcp_setoutput, and that function gets called to actually send data to the outside world.

The KCP structure, and the user data pointer previously passed to the ikcp_create function get passed to the output function you assign.

ikcp_qprint [private]

Looks to be a dummy debug function for printing send and receieve queues. It’s disabled (#if 0).

ikcp_create [PUBLIC]

Allocates an ikcpcb structure. Uses the new/delete previously assigned (malloc/free by default).

typedef struct IKCPCB {
	IUINT32 conv, mtu, mss, state;
	IUINT32 snd_una, snd_nxt, rcv_nxt;
	IUINT32 ts_recent, ts_lastack, ssthresh;
	IINT32 rx_rttval, rx_srtt, rx_rto, rx_minrto;
	IUINT32 snd_wnd, rcv_wnd, rmt_wnd, cwnd, probe;
	IUINT32 current, interval, ts_flush, xmit;
	IUINT32 nrcv_buf, nsnd_buf;
	IUINT32 nrcv_que, nsnd_que;
	IUINT32 nodelay, updated;
	IUINT32 ts_probe, probe_wait;
	IUINT32 dead_link, incr;
	struct IQUEUEHEAD snd_queue;
	struct IQUEUEHEAD rcv_queue;
	struct IQUEUEHEAD snd_buf;
	struct IQUEUEHEAD rcv_buf;
	IUINT32 *acklist;
	IUINT32 ackcount;
	IUINT32 ackblock;
	void *user;
	char *buffer;
	int fastresend;
	int nocwnd, stream;
	int logmask;
	int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user);
	void (*writelog)(const char *log, struct IKCPCB *kcp, void *user);
} ikcpcb;

Zeroes and initializes most things to their respected constants.

buffer mallocates enough space for 3x MTU’s (1400 bytes) with IKCP_OVERHEAD (24 bytes, i.e. 1424 bytes total). Oddly a few lines before the mss gets set to MTU-IKCP_OVERHEAD. So at this time it’s possible the math here is a little off.

Queues and Message Buffers get initialized by calling iqueue_init (snd_queue, rcv_queue, snd_buf, rcv_buf).

Default interval IKCP_INTERVAL is 100 (ms).

ikcp_release [PUBLIC]

While each queue and buffer isn’t empty (snd_queue, rcv_queue, snd_buf, rcv_buf), fetch the next segment, delete the iqueue entry (iqueue_del) and delete the segment (ikcp_segment_delete).

Delete the buffer and acklist (ikcp_free).

Initialize some other variables, and finally call ikcp_free on the kcp structure.

ikcp_peeksize(const ikcpcb* kcp) [PUBLIC]

Summary: Return the number of bytes of the message(s) in the receieve queue (rcv_queue).

Returns -1 if the rcv_queue is empty.

If the queue has a single item, return that items length.

If we don’t have enough fragments (nrcv_queue < seg->frg + 1), the data in the receive buffer should be considered incomplete, and -1 is returned.

Returns the sum of all segment lengths. Early out if the fragment count is zero in a segment.

ikcp_recv(ikcpcb* kcp, char* buffer, int len) [PUBLIC]

Summary: Attempts to fill buffer with the complete stream of data currently sitting in the receieve queue.

If len is negavite, we enable peeking (ispeek). With peeking enabled, the segments will not be consumed after they are written to the buffer (meaning they are consumed in a future call). The negative buffer length is undone a few lines later, and treated as the length thereafter.

Bail if the receieve queue (rcv_queue) is empty.

peeksize is the number of bytes of the data in the receive queue (ikcp_peeksize(kcp)). It doesn’t directly have an effect on the peeking feature above.

If peeksize is negative, it means either the data is empty or incomplete, and the function returns -2.

If peeksize is greater than len, there isn’t enough space to store the data, and the function returns -3.

If the number of items in the receive queue (nrcv_que) is greater than the receive window (rcv_wnd), enable recovery (recover = 1).

Merge all of the fragments together, writing them to buffer. len is reset to 0, and we loop over every fragment until the fragment itself is 0 (nullptr), or if the receive queue is empty (rcv_queue->next == rcv_queue). Fragments are consumed if peeking is disabled.

peeksize and len should match.

We are done this step.

The next step is to movie the next item in the receive buffer (rcv_buf) to the receive queue (rcv_queue).

If there are no items in the receive buffer, then there is nothing to do.

If there are items, the first item is then removed from the receieve buffer (iqueue_del), and it is appended to the receive queue (rcv_queue). Counts are updated.

Something to do with window (both in the loop and

fast recover?

Questions: Are the linked lists circular? Or does rcv_queue->next get set to itself when it is empty?

Answer: YES! The IQUEUE_INITmacro initializes iqueue’s with prev/next pointing to itself.