我当前的系统在Linux上运行,不同的任务使用共享内存来访问公共数据(定义为C结构)。共享数据的大小约为100K。
现在,我想在Windows上运行主用户界面,同时保留Linux中的所有其他任务,并且我正在寻找共享内存的最佳替代品。 UI的刷新率大约是每秒10次。
我怀疑是自己做这件事还是使用第三方解决方案更好。如果我自己做,我的想法是打开一个套接字,然后在客户端(Windows)和服务器(Linux)之间使用某种数据序列化。
在第三方解决方案的情况下,我对选项的数量感到有些不知所措。经过一些搜索,在我看来,正确的解决方案可能是MPI,但我想在开始使用它之前考虑其他选项:XDR,OpenMP,JSON,DBus,RDMA,memcached,boost库......有没有人有任何经验和他们中的任何一个?哪些是使用此类解决方案从Windows访问Linux上的共享内存的优缺点?
也许MPI或其他第三方解决方案过于精细化以至于我的这么简单的使用,我应该使用自己动手的方法?如果我采取这个解决方案,有什么建我错过了什么吗?我看错了方向吗?我不想重新发明轮子。
对于文件共享,我正在考虑Samba。
我的开发是用C和C ++完成的,当然,解决方案需要在Linux和Windows中编译。
答案 0 :(得分:2)
AFAIK(但我不知道Windows)你不能同时在Linux和Windows上share memory(àlashm_overview(7))(你可以使用一些网络协议共享数据,这就是memcached确实如此。
但是,您可以让Linux进程回答来自Windows计算机的网络消息(但这不是共享内存!)。
你考虑使用网络界面吗?您可以在Linux上(使用Linux软件)添加一个带有ajax技术的Web界面,例如:使用FastCGI或HTTP libonion或wt等HTTP服务器库。然后,您的Windows用户可以使用他们的浏览器与您的程序进行交互(它仍然可以在某些Linux计算服务器上运行)。
PS。阅读distributed shared memory上的wikipage。这意味着与你所要求的不同!另请阅读message passing!
答案 1 :(得分:1)
我建议在用户界面和服务之间使用普通的TCP套接字,在两者之间传递简单的标记消息帧,最好是独立于体系结构的方式(即具有特定的字节顺序和整数和浮点类型)。
在服务器端,我使用帮助程序进程或线程,它为每个客户端维护共享状态结构的两个本地副本。第一个反映客户端知道的状态,第二个用于定期快照共享状态。当两者不同时,帮助程序将不同的字段发送给客户端。相反,客户端可以向帮助者发送修改请求。
我不会将100k共享数据结构作为单个块发送;它效率很低。此外,假设状态包含多个字段,让客户端发送整个新的100k状态将覆盖客户端不想要的字段。
我对状态中的每个字段使用一条消息,或者使用原子操作的字段集。消息结构本身应该非常简单。至少,每条消息应以固定大小的长度和类型字段开头,这样接收消息就很容易了。保留以后扩展功能/字段的可能性始终是一个好主意,同时也不会破坏向后兼容性。
您不需要大量代码来实现上述功能。你确实需要一些访问器/操纵器代码用于状态中的每个不同类型的字段(char,short,int,long,double,float和你可能使用的任何其他类型或结构),以及(和比较它的帮助器)具有模拟用户状态的活动状态可能是代码的大部分。
虽然我通常不建议重新发明轮子,但在这种情况下,我建议直接在TCP上编写自己的消息传递代码,因为我所知道的所有库都可以用于此,相当笨拙或者会施加一些设计选择我宁愿留给应用程序。
如果您愿意,我可以提供一些示例代码,希望能够更好地说明这一点。我没有Windows,但Qt确实有一个你可以使用的QTcpSocket类在Linux和Windows中都是一样的,所以不应该有(m)任何差异。事实上,如果你很好地设计了消息结构,你可以用Python之类的其他语言编写UI,而不会对服务器本身产生任何差异。
承诺的例子如下;为最大长度的帖子道歉。
当特定字段有一个编写器(可能是许多读取器)时,配对计数器很有用,并且您希望为写入路径添加最小的开销。读取永远不会阻塞,读者将看到他们是否获得了有效副本,或者是否正在进行更新,他们不应该依赖于他们看到的价值。 (这也为异步信号安全上下文中的字段提供了可靠的写语义,其中pthread锁定不能保证工作。)
此实现最适用于GCC-4.7及更高版本,但也适用于早期的C编译器。旧版__sync内置函数也适用于Intel,Pathscale和PGI C编译器。的 counter.h 强>:
#ifndef COUNTER_H
#define COUNTER_H
/*
* Atomic generation counter support.
*
* A generation counter allows a single writer thread to
* locklessly modify a value non-atomically, while letting
* any number of reader threads detect the change. Reader
* threads can also check if the value they observed is
* "atomic", or taken during a modification in progress.
*
* There is no protection against multiple concurrent writers.
*
* If the writer gets canceled or dies during an update,
* the counter will get garbled. Reinitialize before relying
* on such a counter.
*
* There is no guarantee that a reader will observe an
* "atomic" value, if the writer thread modifies the value
* more often than the reader thread can read it.
*
* Two typical use cases:
*
* A) A single writer requires minimum overhead/latencies,
* whereas readers can poll and retry if necessary
*
* B) Non-atomic value or structure modified in
* an interrupt context (async-signal-safe)
*
* Initialization:
*
* Counter counter = COUNTER_INITIALIZER;
*
* or
*
* Counter counter;
* counter_init(&counter);
*
* Write sequence:
*
* counter_acquire(&counter);
* (modify or write value)
* counter_release(&counter);
*
* Read sequence:
*
* unsigned int check;
* check = counter_before(&counter);
* (obtain a copy of the value)
* if (check == counter_after(&counter))
* (a copy of the value is atomic)
*
* Read sequence with spin-waiting,
* will spin forever if counter garbled:
*
* unsigned int check;
* do {
* check = counter_before(&counter);
* (obtain a copy of the value)
* } while (check != counter_after(&counter));
*
* All these are async-signal-safe, and will never block
* (except for the spinning loop just above, obviously).
*/
typedef struct {
volatile unsigned int value[2];
} Counter;
#define COUNTER_INITIALIZER {{0U,0U}}
#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7)
/*
* GCC 4.7 and later provide __atomic built-ins.
* These are very efficient on x86 and x86-64.
*/
static inline void counter_init(Counter *const counter)
{
/* This is silly; assignments should suffice. */
do {
__atomic_store_n(&(counter->value[1]), 0U, __ATOMIC_SEQ_CST);
__atomic_store_n(&(counter->value[0]), 0U, __ATOMIC_SEQ_CST);
} while (__atomic_load_n(&(counter->value[0]), __ATOMIC_SEQ_CST) |
__atomic_load_n(&(counter->value[1]), __ATOMIC_SEQ_CST));
}
static inline unsigned int counter_before(Counter *const counter)
{
return __atomic_load_n(&(counter->value[1]), __ATOMIC_SEQ_CST);
}
static inline unsigned int counter_after(Counter *const counter)
{
return __atomic_load_n(&(counter->value[0]), __ATOMIC_SEQ_CST);
}
static inline unsigned int counter_acquire(Counter *const counter)
{
return __atomic_fetch_add(&(counter->value[0]), 1U, __ATOMIC_SEQ_CST);
}
static inline unsigned int counter_release(Counter *const counter)
{
return __atomic_fetch_add(&(counter->value[1]), 1U, __ATOMIC_SEQ_CST);
}
#else
/*
* Rely on legacy __sync built-ins.
*
* Because there is no __sync_fetch(),
* counter_before() and counter_after() are safe,
* but not optimal (especially on x86 and x86-64).
*/
static inline void counter_init(Counter *const counter)
{
/* This is silly; assignments should suffice. */
do {
counter->value[0] = 0U;
counter->value[1] = 0U;
__sync_synchronize();
} while (__sync_fetch_and_add(&(counter->value[0]), 0U) |
__sync_fetch_and_add(&(counter->value[1]), 0U));
}
static inline unsigned int counter_before(Counter *const counter)
{
return __sync_fetch_and_add(&(counter->value[1]), 0U);
}
static inline unsigned int counter_after(Counter *const counter)
{
return __sync_fetch_and_add(&(counter->value[0]), 0U);
}
static inline unsigned int counter_acquire(Counter *const counter)
{
return __sync_fetch_and_add(&(counter->value[0]), 1U);
}
static inline unsigned int counter_release(Counter *const counter)
{
return __sync_fetch_and_add(&(counter->value[1]), 1U);
}
#endif
#endif /* COUNTER_H */
每个共享状态字段类型都需要定义自己的结构。出于我们的目的,我只使用Value
结构来描述其中一个。您可以自定义所有需要的字段和值类型以满足您的需求。例如,双精度浮点分量的3D矢量将是
typedef struct {
double x;
double y;
double z;
} Value;
typedef struct {
Counter counter;
Value value;
} Field;
修改线程使用的地方,例如
void set_field(Field *const field, Value *const value)
{
counter_acquire(&field->counter);
field->value = value;
counter_release(&field->counter);
}
并且每个读者使用例如
维护他们自己的本地shapshottypedef enum {
UPDATED = 0,
UNCHANGED = 1,
BUSY = 2
} Updated;
Updated check_field(Field *const local, const Field *const shared)
{
Field cache;
cache.counter[0] = counter_before(&shared->counter);
/* Local counter check allows forcing an update by
* simply changing one half of the local counter.
* If you don't need that, omit the local-only part. */
if (local->counter[0] == local->counter[1] &&
cache.counter[0] == local->counter[0])
return UNCHANGED;
cache.value = shared->value;
cache.counter[1] = counter_after(&shared->counter);
if (cache.counter[0] != cache.counter[1])
return BUSY;
*local = cache;
return UPDATED;
}
请注意,上面的cache
构成了该值的一个本地副本,因此每个读者只需要维护它感兴趣的字段的一个副本。此外,上述访问者函数仅修改local
成功快照。
使用pthread锁定时,pthread_rwlock_t
是一个不错的选择,只要程序员理解多个读者和编写者的优先级问题。 (see man pthread_rwlock_rdlock
和man pthread_rwlock_wrlock
了解详情。)
就个人而言,我只会尽可能缩短保持时间,以降低总体复杂性。用于受互斥保护的变更计数值I' d的字段结构可能类似于
typedef struct {
pthread_mutex_t mutex;
Value value;
volatile unsigned int change;
} Field;
void set_field(Field *const field, const Value *const value)
{
pthread_mutex_lock(&field->mutex);
field->value = *value;
#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7)
__atomic_fetch_add(&field->change, 1U, __ATOMIC_SEQ_CST);
#else
__sync_fetch_and_add(&field->change, 1U);
#endif
pthread_mutex_unlock(&field->mutex);
}
void get_field(Field *const local, Field *const field)
{
pthread_mutex_lock(&field->mutex);
local->value = field->value;
#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7)
local->value = __atomic_fetch_n(&field->change, __ATOMIC_SEQ_CST);
#else
local->value = __sync_fetch_and_add(&field->change, 0U);
#endif
pthread_mutex_unlock(&field->mutex);
}
Updated check_field(Field *const local, Field *const shared)
{
#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7)
if (local->change == __atomic_fetch_n(&field->change, __ATOMIC_SEQ_CST))
return UNCHANGED;
#else
if (local->change == __sync_fetch_and_add(&field->change, 0U))
return UNCHANGED;
#endif
pthread_mutex_lock(&shared->mutex);
local->value = shared->value;
#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7)
local->change = __atomic_fetch_n(&field->change, __ATOMIC_SEQ_CST);
#else
local->change = __sync_fetch_and_add(&field->change, 0U);
#endif
pthread_mutex_unlock(&shared->mutex);
}
请注意,本地副本中的互斥锁字段未使用,可以从本地副本中省略(如果没有它,则定义合适的Cache等结构类型)。
关于 change 字段的语义很简单:它总是以原子方式访问,因此可以在不保持互斥锁的情况下读取。它仅在保持互斥锁时被修改(但由于读者不持有互斥锁,因此必须以原子方式对其进行修改)。
请注意,如果您的服务器仅限于x86或x86-64体系结构,则可以使用普通访问上面的更改(例如field->change++
),因为unsigned int
访问权限是这些架构上的原子;无需使用内置函数。 (虽然增量是非原子的,但读者只能看到旧的unsigned int
,所以这也不是问题。)
在服务器上,您需要某种代理商"进程或线程,代表远程客户端,向每个客户端发送字段更改,并可选择从每个客户端接收字段内容以进行更新。
有效实施此类代理是一个大问题。经过深思熟虑的建议需要有关系统的更详细信息。我想,最简单的可能实现是一个低优先级的线程,只需连续扫描共享状态,报告每个客户端看到的任何字段更改。如果变化率很高 - 比方说,通常在整个州每秒进行十几次变化 - 那么这可能是完全可以接受的。如果变化率变化,则添加全局变更计数器(在修改单个字段时修改,即在set_field()
中)和在没有观察到全局变化时休眠(或至少屈服)更好。
如果有多个远程客户端,我会使用单个代理,它将更改收集到队列中,并在队列的头部看到状态的缓存副本(之前的更改是指向它的那些更改) )。最初连接的客户端获取整个状态,然后每个排队的更改。请注意,队列条目不需要像这样表示字段,而是发送到每个客户端以更新该字段的消息,如果后一个更新替换已经排队的值,则可以删除已排队的值,从而减少需要发送到正在追赶的客户端的数据。这个问题本身就是一个有趣的话题,在我看来,这个问题值得我自己提出。
就像我上面提到的,我个人会在用户界面和服务之间使用普通的TCP套接字。
TCP消息有四个主要问题:
字节顺序和数据格式(除非服务器和远程客户端都保证使用相同的硬件架构)
我首选的解决方案是在发送时使用本机字节顺序(但定义良好的标准类型),收件人执行任何字节顺序转换。这要求每个末端发送一条初始消息,其中包含预定的"原型值" ,用于每个使用的整数和浮点类型。
我假设整数使用两个补码格式而没有填充位,而double和float分别是双精度和单精度IEEE-754类型。大多数当前的硬件架构本身具有这些(尽管字节排序确实不同)。奇怪的架构可以并且通过提供合适的命令行开关或使用一些库来模拟这些架构。
消息框架(从应用程序的角度来看,TCP是一个字节流,而不是数据包流)
最强大的选项是为每条消息添加固定大小类型字段和固定大小长度字段。 (通常,长度字段表示此消息中有多少附加字节,并且将包含发送方可能添加的任何可能的填充字节。)
收件人将收到TCP输入,直到它有足够的缓存固定类型和长度部分来检查完整的数据包长度。然后它接收更多数据,直到它有一个完整的TCP数据包。最小的接收缓冲区实现类似于
扩展
向后兼容性对于简化更新和升级至关重要。这意味着您可能需要在从服务器到客户端的初始消息中包含某种版本号,并让服务器和客户端忽略他们不知道的消息。 (这显然需要每条消息中都有一个长度字段,因为收件人无法推断出他们无法识别的消息长度。)
在某些情况下,您可能需要能够将邮件标记为重要邮件,因为如果收件人无法识别邮件,则应中止进一步的通信。通过在类型字段中使用某种标识符或位,这是最容易实现的。例如,PNG file format非常类似于此处描述的消息格式,具有四字节(四个ASCII字符)类型字段,每个字节长度为四字节(32位)字段消息(" chunk" )。如果第一个类型字符是大写ASCII,则表示它是一个关键字符。
传递初始状态
将初始状态作为单个块传递可修复整个共享状态结构。相反,我建议单独传递每个字段或记录,就像它们是更新一样。可选地,当所有初始字段都已发送时,您可以发送一条消息,通知客户端他们拥有整个状态;这应该让客户端首先接收完整状态,然后构造并呈现用户界面,而不必动态调整到不同数量的字段。
另一个问题是,每个客户端是需要完整状态还是仅需要较小的状态子集。当远程客户端连接到服务器时,它可能应该包含服务器可以用来做出该决定的某种标识符。
这是 messages.h ,一个头文件,内联实现整数和浮点类型处理和字节顺序检测:
#ifndef MESSAGES_H
#define MESSAGES_H
#include <stdint.h>
#include <string.h>
#include <errno.h>
#if defined(__GNUC__)
static const struct __attribute__((packed)) {
#else
#pragma pack(push,1)
#endif
const uint64_t u64; /* Offset +0 */
const int64_t i64; /* +8 */
const double dbl; /* +16 */
const uint32_t u32; /* +24 */
const int32_t i32; /* +28 */
const float flt; /* +32 */
const uint16_t u16; /* +36 */
const int16_t i16; /* +38 */
} prototypes = {
18364758544493064720ULL, /* u64 */
-1311768465156298103LL, /* i64 */
0.71948481353325643983254167324048466980457305908203125,
3735928559UL, /* u32 */
-195951326L, /* i32 */
1.06622731685638427734375f, /* flt */
51966U, /* u16 */
-7658 /* i16 */
};
#if !defined(__GNUC__)
#pragma pack(pop)
#endif
/* Add prototype section to a buffer.
*/
size_t add_prototypes(unsigned char *const data, const size_t size)
{
if (size < sizeof prototypes) {
errno = ENOSPC;
return 0;
} else {
memcpy(data, &prototypes, sizeof prototypes);
return sizeof prototypes;
}
}
/*
* Byte order manipulation functions.
*/
static void reorder64(void *const dst, const void *const src, const int order)
{
if (order) {
uint64_t value;
memcpy(&value, src, 8);
if (order & 1)
value = ((value >> 8U) & 0x00FF00FF00FF00FFULL)
| ((value & 0x00FF00FF00FF00FFULL) << 8U);
if (order & 2)
value = ((value >> 16U) & 0x0000FFFF0000FFFFULL)
| ((value & 0x0000FFFF0000FFFFULL) << 16U);
if (order & 4)
value = ((value >> 32U) & 0x00000000FFFFFFFFULL)
| ((value & 0x00000000FFFFFFFFULL) << 32U);
memcpy(dst, &value, 8);
} else
if (dst != src)
memmove(dst, src, 8);
}
static void reorder32(void *const dst, const void *const src, const int order)
{
if (order) {
uint32_t value;
memcpy(&value, src, 4);
if (order & 1)
value = ((value >> 8U) & 0x00FF00FFUL)
| ((value & 0x00FF00FFUL) << 8U);
if (order & 2)
value = ((value >> 16U) & 0x0000FFFFUL)
| ((value & 0x0000FFFFUL) << 16U);
memcpy(dst, &value, 4);
} else
if (dst != src)
memmove(dst, src, 4);
}
static void reorder16(void *const dst, const void *const src, const int order)
{
if (order & 1) {
const unsigned char c[2] = { ((const unsigned char *)src)[0],
((const unsigned char *)src)[1] };
((unsigned char *)dst)[0] = c[1];
((unsigned char *)dst)[1] = c[0];
} else
if (dst != src)
memmove(dst, src, 2);
}
/* Detect byte order conversions needed.
*
* If the prototypes cannot be supported, returns -1.
*
* If prototype record uses native types, returns 0.
*
* Otherwise, bits 0..2 are the integer order conversion,
* and bits 3..5 are the floating-point order conversion.
* If 'order' is the return value, use
* reorderXX(local, remote, order)
* for integers, and
* reorderXX(local, remote, order/8)
* for floating-point types.
*
* For adjusting records sent to server, just do the same,
* but with order obtained by calling this function with
* parameters swapped.
*/
int detect_order(const void *const native, const void *const other)
{
const unsigned char *const source = other;
const unsigned char *const target = native;
unsigned char temp[8];
int iorder = 0;
int forder = 0;
/* Verify the size of the types.
* C89/C99/C11 says sizeof (char) == 1, but check that too. */
if (sizeof (double) != 8 ||
sizeof (int64_t) != 8 ||
sizeof (uint64_t) != 8 ||
sizeof (float) != 4 ||
sizeof (int32_t) != 4 ||
sizeof (uint32_t) != 4 ||
sizeof (int16_t) != 2 ||
sizeof (uint16_t) != 2 ||
sizeof (unsigned char) != 1 ||
sizeof prototypes != 40)
return -1;
/* Find byte order for the largest floating-point type. */
while (1) {
reorder64(temp, source + 16, forder);
if (!memcmp(temp, target + 16, 8))
break;
if (++forder >= 8)
return -1;
}
/* Verify forder works for all other floating-point types. */
reorder32(temp, source + 32, forder);
if (memcmp(temp, target + 32, 4))
return -1;
/* Find byte order for the largest integer type. */
while (1) {
reorder64(temp, source + 0, iorder);
if (!memcmp(temp, target + 0, 8))
break;
if (++iorder >= 8)
return -1;
}
/* Verify iorder works for all other integer types. */
reorder64(temp, source + 8, iorder);
if (memcmp(temp, target + 8, 8))
return -1;
reorder32(temp, source + 24, iorder);
if (memcmp(temp, target + 24, 4))
return -1;
reorder32(temp, source + 28, iorder);
if (memcmp(temp, target + 28, 4))
return -1;
reorder16(temp, source + 36, iorder);
if (memcmp(temp, target + 36, 2))
return -1;
reorder16(temp, source + 38, iorder);
if (memcmp(temp, target + 38, 2))
return -1;
/* Everything works. */
return iorder + 8 * forder;
}
/* Verify current architecture is supportable.
* This could be a compile-time check.
*
* (The buffer contains prototypes for network byte order,
* and actually returns the order needed to convert from
* native to network byte order.)
*
* Returns -1 if architecture is not supported,
* a nonnegative (0 or positive) value if successful.
*/
static int verify_architecture(void)
{
static const unsigned char network_endian[40] = {
254U, 220U, 186U, 152U, 118U, 84U, 50U, 16U, /* u64 */
237U, 203U, 169U, 135U, 238U, 204U, 170U, 137U, /* i64 */
63U, 231U, 6U, 5U, 4U, 3U, 2U, 1U, /* dbl */
222U, 173U, 190U, 239U, /* u32 */
244U, 82U, 5U, 34U, /* i32 */
63U, 136U, 122U, 35U, /* flt */
202U, 254U, /* u16 */
226U, 22U, /* i16 */
};
return detect_order(&prototypes, network_endian);
}
#endif /* MESSAGES_H */
这是一个示例函数,发送方可以使用该函数将由32位无符号整数标识的3分量向量打包成36字节的消息。这使用以四个字节(&#34; Vec3
&#34;)开头的消息帧,后跟32位帧长度,后跟32位标识符,后跟三个双精度帧:
size_t add_vector(unsigned char *const data, const size_t size,
const uint32_t id,
const double x, const double y, const double z)
{
const uint32_t length = 4 + 4 + 4 + 8 + 8 + 8;
if (size < (size_t)bytes) {
errno = ENOSPC;
return 0;
}
/* Message identifier, four bytes */
buffer[0] = 'V';
buffer[1] = 'e';
buffer[2] = 'c';
buffer[3] = '3';
/* Length field, uint32_t */
memcpy(buffer + 4, &length, 4);
/* Identifier, uint32_t */
memcpy(buffer + 8, &id, 4);
/* Vector components */
memcpy(buffer + 12, &x, 8);
memcpy(buffer + 20, &y, 8);
memcpy(buffer + 28, &z, 8);
return length;
}
就个人而言,我更喜欢较短的16位标识符和16位长度;它还将最大消息长度限制为65536字节,使得65536字节成为良好的读/写缓冲区大小。
收件人可以处理收到的TCP数据流,例如:
static unsigned char *input_data; /* Data buffer */
static size_t input_size; /* Data buffer size */
static unsigned char *input_head; /* Next byte in buffer */
static unsigned char *input_tail; /* After last buffered byte */
static int input_order; /* From detect_order() */
/* Function to handle "Vec3" messages: */
static void handle_vec3(unsigned char *const msg)
{
uint32_t id;
double x, y, z;
reorder32(&id, msg+8, input_order);
reorder64(&x, msg+12, input_order/8);
reorder64(&y, msg+20, input_order/8);
reorder64(&z, msg+28, input_order/8);
/* Do something with vector id, x, y, z. */
}
/* Function that tries to consume thus far buffered
* input data -- typically run once after each
* successful TCP receive. */
static void consume(void)
{
while (input_head + 8 < input_tail) {
uint32_t length;
/* Get current packet length. */
reorder32(&length, input_head + 4, input_order);
if (input_head + length < input_tail)
break; /* Not all read, yet. */
/* We have a full packet! */
/* Handle "Vec3" packet: */
if (input_head[0] == 'V' &&
input_head[1] == 'e' &&
input_head[2] == 'c' &&
input_head[3] == '3')
handle_vec3(input_head);
/* Advance to next packet. */
input_head += length;
}
if (input_head < input_tail) {
/* Move partial message to start of buffer. */
if (input_head > input_data) {
const size_t have = input_head - input_data;
memmove(input_data, input_head, have);
input_head = input_data;
input_tail = input_data + have;
}
} else {
/* Buffer is empty. */
input_head = input_data;
input_tail = input_data;
}
}
有问题吗?