Peer-to-peer GUI adapter

Discussion of chess software programming and technical issues.

Moderators: hgm, Rebel, chrisw

User avatar
hgm
Posts: 27788
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Peer-to-peer GUI adapter

Post by hgm »

I made some progress with the p2p pseudo-engine. In particular, I can now use it to play games between two WinBoard instances running on the same machine, connecting to 'localhost' (IP address 127.0.0.1). After loading p2p.exe as first engine, I can go to the Engine Settings dialog:

Image

There I can enter the IP address, and press the Connect button, and it will make a connection. After that, I can start playing, and if the other side accepts the challenge (presented in a GUI popup) by starting the complementary game.

Image

It is also possible to exchange text messages, either by writing one in the Engine Settings dialog and pressing 'Send line', or leaving it empty and press 'Send line', after which a GUI popup will prompt you for a message. (Advantage of the latter is that you don't have to clear the previous line all the time.)

Image

Problem is that it does not seem to work between different machines. A few seconds after pressing 'Connect' it presents a popup telling me it could not connect with the server. :cry:

Am I correct in assuming that this must be some firewall problem I do not master yet? If things work on 127.0.0.1, aren't they supposed to work for any IP address? Both computers are on my LAN, and have IP addresses like 192.168.2.x there. As far as I could see I did disable the firewalls on both. Yet, in either direction the connection is refused.

Any suggestions? I am an absolute noob where it comes to network programming...
User avatar
hgm
Posts: 27788
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Peer-to-peer GUI adapter

Post by hgm »

Btw, my (Windows) connetcion routines look like this now:

Code: Select all

int
MakeConnection (char *host, int port, SOCKET *s) 
{   // returns 0 on success, with socket in s that is TCP-connected to host
    SOCKET ConnectSocket = INVALID_SOCKET;
    struct sockaddr_in sa;
    char *sendbuf = "this is a test";
    char recvbuf[DEFAULT_BUFLEN];
    int iResult;
    int recvbuflen = DEFAULT_BUFLEN;
    
    // Create a SOCKET for connecting to server
    ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (ConnectSocket == INVALID_SOCKET) return Error(3, WSAGetLastError(), INVALID_SOCKET);

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr(host);
    sa.sin_port = htons(port);

    // Connect to server.
    if (connect( ConnectSocket, (SOCKADDR *) &sa, sizeof(sa)) == SOCKET_ERROR) return Error(4, 0, INVALID_SOCKET);

    *s = ConnectSocket;
    return 0; // success
}

int
SetupServer (char *port, SOCKET *s)
{
    SOCKET ListenSocket = INVALID_SOCKET;
    struct addrinfo hints;
    struct sockaddr_in sa;
    
    // Create a SOCKET for connecting to server
    ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (ListenSocket == INVALID_SOCKET) return Error(3, WSAGetLastError(), INVALID_SOCKET);

    // Setup the TCP listening socket
    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr("127.0.0.1");
    sa.sin_port = htons(atoi(port));
    if (bind( ListenSocket, (SOCKADDR *) &sa, sizeof(sa)) == SOCKET_ERROR) return Error(7, WSAGetLastError(), ListenSocket);

    *s = ListenSocket;
    return 0;
}

int
WaitForCaller (SOCKET ListenSocket, SOCKET *s)
{
    SOCKET ClientSocket = INVALID_SOCKET;
    struct sockaddr_in sa;
    
    printf("# listening\n"), fflush(stdout);
    if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR) return Error(8, WSAGetLastError(), ListenSocket);
    printf("# done listening\n"), fflush(stdout);

    // Accept a client socket
    *s = ClientSocket = accept(ListenSocket, NULL, NULL);
    if (ClientSocket == INVALID_SOCKET) return Error(9, WSAGetLastError(), ListenSocket);
    printf("# accepted incoming call\n"), fflush(stdout);

    return 0; // success
}
F. Bluemers
Posts: 868
Joined: Thu Mar 09, 2006 11:21 pm
Location: Nederland

Re: Peer-to-peer GUI adapter

Post by F. Bluemers »

yes,you have to open the port 27015 or whatever port you use in the firewall.
User avatar
hgm
Posts: 27788
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Peer-to-peer GUI adapter

Post by hgm »

It also seems to be a problem that I specified "127.0.0.1" when binding the ListingSocket. When I assign NULL to s_addr, I can connect!

It still does not work, though, because when I send from machine to machine the packets are apparently broken up or joined, so that the recv call does not return full lines. This wrecks my code completely! So I have to accumulate and join packets until they contain a newline, and save the leftover beyond it. This did not happen when they communicate through localhost.
F. Bluemers
Posts: 868
Joined: Thu Mar 09, 2006 11:21 pm
Location: Nederland

Re: Peer-to-peer GUI adapter

Post by F. Bluemers »

hgm wrote:It also seems to be a problem that I specified "127.0.0.1" when binding the ListingSocket. When I assign NULL to s_addr, I can connect!

It still does not work, though, because when I send from machine to machine the packets are apparently broken up or joined, so that the recv call does not return full lines. This wrecks my code completely! So I have to accumulate and join packets until they contain a newline, and save the leftover beyond it. This did not happen when they communicate through localhost.
maybe you should try blocking mode reads?
http://msdn.microsoft.com/en-us/library ... s.85).aspx
Michel
Posts: 2272
Joined: Mon Sep 29, 2008 1:50 am

Re: Peer-to-peer GUI adapter

Post by Michel »

It also seems to be a problem that I specified "127.0.0.1" when binding the ListingSocket. When I assign NULL to s_addr, I can connect!
That is expected. If you listen only on the interface with address 127.0.0.1 (normally localhost) it is normal that you only receive
connections coming through the interface 127.0.0.1....
It still does not work, though, because when I send from machine to machine the packets are apparently broken up or joined, so that the recv call does not return full lines. This wrecks my code completely! So I have to accumulate and join packets until they contain a newline, and save the leftover beyond it. This did not happen when they communicate through localhost.
This is expected as well.
User avatar
hgm
Posts: 27788
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Peer-to-peer GUI adapter

Post by hgm »

OK, both problems should be fixed now. I could play a game between two machines on my LAN.

I uploaded the beta version to my website, in case anyone feels like trying it out with their favorite GUI:

http://hgm.nubati.net/p2p.zip

Just install p2p.exe like any other WinBoard engine.
User avatar
hgm
Posts: 27788
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Peer-to-peer GUI adapter

Post by hgm »

Btw, I intend the protocol for this to be open. At the moment what I am using has the following specs:
PEER-TO_PEER PROTOCOL SPECS

The peer-to-peer protocol is designed for connecting two Chess GUIs over
the internet directly, i.e. without using a third-party server as
intermediate. It uses a TCP/IP connection. At the lowest level one of the
GUIs would act as a server, listening on a certain port of the machine it
is started on for incoming connection request. The other side ('client')
would then send such a request to that machine and port, upon which the
connection is established, and client and server can start to exchange
(text) messages.

GUIs could use the protocol directly, but also through an adapter that to
them looks like a normal (WB) engine. For this reason the protocol is
designed to not need direct control over the GUI at the other end (as
engines would not have that). Of course when implemented directly in a GUI
some of the things that the protocol implements as requests can be obeyed
automatically.

The basic idea of the peer-to-peer protocol is that both sides maintain an
up-to-date copy of the state of the GUI at the remote end, which is
allowed to be different from the state of the local GUI. So any state
change (move, takeback, start of a new game, setting up of a position,
time control setting) is immediately relayed to the peer. Whether the peer
does anything with that information other than updating the state copy
depends on whether it is in 'playing' or 'setup' mode.
Initially (after connecting, or finishing a previous game) both sides
are in setup mode, so the users can set up a game without disturbing each
other. (E.g. select TC, variant, load an adjourned game from PGN, etc.)
When the user wants to start playing (e.g. by pressing Machine White or
Machine Black), a 'go' message is sent to the peer. Only when a 'go' has
been sent to as well as received from the peer there can be a transition
to 'playing' mode, and only when the local and remote GUI have matching
state. This can only be checked on the second 'go', and the design choice
has been made to let the GUI receiving that do the checking. So that if
the states do not match, it can immediately interpret it as a counter
proposal, present that to its user (e.g. in a popup), so that the burden
to match the remote state or reject it is now on him again. This way the
users take turns in sending 'go' commands that reject the GUI state of
their peer. Until one finally sets up the requested state before sending
the next 'go'. This 'go' will then be answered with an 'accepted', and
both the sender and receiver of that 'accepted' then know they are in
playing mode.
After that, it is just a matter of relaying moves, playing moves entered
by the remote during his turn also on our side, and vice versa. A 'result'
command makes both sides drop out of playing mode.

Detailed specification of p2p commands:

new
Sent when the user starts a new game (e.g. when a 'new' command is
received from the GUI). Unlike the corresponding command of WB protocol,
however, the receiver must assume the sender is now in force mode.

variant VARIANTNAME
Sent after 'new' when the started game is not normal Chess. Used to update
the state copy of the remote GUI.

setboard FEN
Sent after 'new' or 'variant' to update the state-copy of the remote GUI.

move MOVE
Sent whenever the GUI on the side of the sender advances one move. This
can be because the adapter received a 'usermove MOVE' command from that
GUI, or because you sent a 'move MOVE' command to that GUI. In the latter
case you would be echoing a move command received earlier from your peer.
Upon reception, you forward the command to the GUI only if it was a move
of the side the adapter is now playing (and in that case echo it back to
the peer). Otherwise (i.e. when in force mode or for a move of the wrong
side) you just use it to update your state copy of the remote GUI.

go
Sent whenever you start playing. Which is when you receive a 'go' command
from your GUI, or when you receive the first move of a game from the GUI
when you were set to play black by a 'new' command. In the latter case you
send the 'go' after relaying the move.
Upon reception, you know the remote GUI has started a game, and is now
expecting a move from you. If you were already playing (having sent a 'go'
command before) a compatible game (i.e. opposite color, same variant,
equal forced moves, if any) you should have the move ready, and the remote
adapter should already have a copy of it, and you reply with 'accepted' to
inform him the game has started, and he can play that move.
If you were not yet playing, (i.e. had not sent a 'go' yet), or the
games are not compatible (your GUI's state does not match the copy you
maintain of the remote GUI's state), you request the user to start a
compatible game (e.g. by sending a 'telluser' command to your GUI). An
already started game should be aborted (e.g. by sending a 'result' command
to the GUI), so the user can set up a compatible game.

accepted
Sent when you receive a 'go' from your peer, after already having sent one
yourself, when the games matches.
Upon reception you can release the move you had already in the state
copy of the remote GUI, if it is not in your own game yet, by sending it
to your GUI (and to your peer!). Note that you should not start playing
merely because the user on your side orders it (i.e. when you receive a
'go' from the GUI), but that you forward this 'go' to your peer (for
compatibility checking) and receive it back as 'accepted', upon which you
act.

name OPPONENTNAME
Sent to inform the peer of the name of his opponent. Upon reception it
could be used to alter the name the adapter presents to its GUI (through a
'feature myname="OPPONENTNAME"').

text MESSAGE
Sent when you want the MESSAGE displayed to the user (e.g. through a
'telluser' command to the GUI at the receiving end).

hallo MESSAGE
Sent after answering an incoming call, to confirm your presence. Upon
reception the MESSAGE would typically be displayed to the user, so he
knows he is now connected, and to whom.

level MPS TC INC
Sent whenever the user specifies a time control, so that the receiving
side can update its state copy of the remote GUI, and later use it for
checking compatibility of started games. The parameters have their usual
WB-protocol meaning, (but TC is in seconds!), with one kludge: if MPS = -
1, it specifies fixed time per move, given by TC.

time T
otim T
Commands to relay the times left on the clock (in centi-sec) on the side
of the sender to the peer. The latter could use them to synchronize (on
of) his clocks, or request a user to do so when the difference gets out of
hand.

remove
Sent when the user has performed a (2-ply) takeback, from a position where
he had the move. Upon reception we should request the user on our side to
do the same (or perhaps force it on him).

undo
Sent (twice) as confirmation of the 'remove' command from peer. Upon
reception the latter can update his state copy of the remote GUI by taking
back one ply. In addition he should now assume the remote GUI has dropped
out of playing mode, and will try to renegociate a game start by sending a
new 'go'.

quit
Sent as last command before closing a connection, so that the peer knows
the connection was terminated intentionally.
ZirconiumX
Posts: 1334
Joined: Sun Jul 17, 2011 11:14 am

Re: Peer-to-peer GUI adapter

Post by ZirconiumX »

hgm wrote:Btw, I intend the protocol for this to be open. At the moment what I am using has the following specs:
PEER-TO_PEER PROTOCOL SPECS

The peer-to-peer protocol is designed for connecting two Chess GUIs over
the internet directly, i.e. without using a third-party server as
intermediate. It uses a TCP/IP connection. At the lowest level one of the
GUIs would act as a server, listening on a certain port of the machine it
is started on for incoming connection request. The other side ('client')
would then send such a request to that machine and port, upon which the
connection is established, and client and server can start to exchange
(text) messages.

GUIs could use the protocol directly, but also through an adapter that to
them looks like a normal (WB) engine. For this reason the protocol is
designed to not need direct control over the GUI at the other end (as
engines would not have that). Of course when implemented directly in a GUI
some of the things that the protocol implements as requests can be obeyed
automatically.

The basic idea of the peer-to-peer protocol is that both sides maintain an
up-to-date copy of the state of the GUI at the remote end, which is
allowed to be different from the state of the local GUI. So any state
change (move, takeback, start of a new game, setting up of a position,
time control setting) is immediately relayed to the peer. Whether the peer
does anything with that information other than updating the state copy
depends on whether it is in 'playing' or 'setup' mode.
Initially (after connecting, or finishing a previous game) both sides
are in setup mode, so the users can set up a game without disturbing each
other. (E.g. select TC, variant, load an adjourned game from PGN, etc.)
When the user wants to start playing (e.g. by pressing Machine White or
Machine Black), a 'go' message is sent to the peer. Only when a 'go' has
been sent to as well as received from the peer there can be a transition
to 'playing' mode, and only when the local and remote GUI have matching
state. This can only be checked on the second 'go', and the design choice
has been made to let the GUI receiving that do the checking. So that if
the states do not match, it can immediately interpret it as a counter
proposal, present that to its user (e.g. in a popup), so that the burden
to match the remote state or reject it is now on him again. This way the
users take turns in sending 'go' commands that reject the GUI state of
their peer. Until one finally sets up the requested state before sending
the next 'go'. This 'go' will then be answered with an 'accepted', and
both the sender and receiver of that 'accepted' then know they are in
playing mode.
After that, it is just a matter of relaying moves, playing moves entered
by the remote during his turn also on our side, and vice versa. A 'result'
command makes both sides drop out of playing mode.

Detailed specification of p2p commands:

new
Sent when the user starts a new game (e.g. when a 'new' command is
received from the GUI). Unlike the corresponding command of WB protocol,
however, the receiver must assume the sender is now in force mode.

variant VARIANTNAME
Sent after 'new' when the started game is not normal Chess. Used to update
the state copy of the remote GUI.

setboard FEN
Sent after 'new' or 'variant' to update the state-copy of the remote GUI.

move MOVE
Sent whenever the GUI on the side of the sender advances one move. This
can be because the adapter received a 'usermove MOVE' command from that
GUI, or because you sent a 'move MOVE' command to that GUI. In the latter
case you would be echoing a move command received earlier from your peer.
Upon reception, you forward the command to the GUI only if it was a move
of the side the adapter is now playing (and in that case echo it back to
the peer). Otherwise (i.e. when in force mode or for a move of the wrong
side) you just use it to update your state copy of the remote GUI.

go
Sent whenever you start playing. Which is when you receive a 'go' command
from your GUI, or when you receive the first move of a game from the GUI
when you were set to play black by a 'new' command. In the latter case you
send the 'go' after relaying the move.
Upon reception, you know the remote GUI has started a game, and is now
expecting a move from you. If you were already playing (having sent a 'go'
command before) a compatible game (i.e. opposite color, same variant,
equal forced moves, if any) you should have the move ready, and the remote
adapter should already have a copy of it, and you reply with 'accepted' to
inform him the game has started, and he can play that move.
If you were not yet playing, (i.e. had not sent a 'go' yet), or the
games are not compatible (your GUI's state does not match the copy you
maintain of the remote GUI's state), you request the user to start a
compatible game (e.g. by sending a 'telluser' command to your GUI). An
already started game should be aborted (e.g. by sending a 'result' command
to the GUI), so the user can set up a compatible game.

accepted
Sent when you receive a 'go' from your peer, after already having sent one
yourself, when the games matches.
Upon reception you can release the move you had already in the state
copy of the remote GUI, if it is not in your own game yet, by sending it
to your GUI (and to your peer!). Note that you should not start playing
merely because the user on your side orders it (i.e. when you receive a
'go' from the GUI), but that you forward this 'go' to your peer (for
compatibility checking) and receive it back as 'accepted', upon which you
act.

name OPPONENTNAME
Sent to inform the peer of the name of his opponent. Upon reception it
could be used to alter the name the adapter presents to its GUI (through a
'feature myname="OPPONENTNAME"').

text MESSAGE
Sent when you want the MESSAGE displayed to the user (e.g. through a
'telluser' command to the GUI at the receiving end).

hallo MESSAGE
Sent after answering an incoming call, to confirm your presence. Upon
reception the MESSAGE would typically be displayed to the user, so he
knows he is now connected, and to whom.

level MPS TC INC
Sent whenever the user specifies a time control, so that the receiving
side can update its state copy of the remote GUI, and later use it for
checking compatibility of started games. The parameters have their usual
WB-protocol meaning, (but TC is in seconds!), with one kludge: if MPS = -
1, it specifies fixed time per move, given by TC.

time T
otim T
Commands to relay the times left on the clock (in centi-sec) on the side
of the sender to the peer. The latter could use them to synchronize (on
of) his clocks, or request a user to do so when the difference gets out of
hand.

remove
Sent when the user has performed a (2-ply) takeback, from a position where
he had the move. Upon reception we should request the user on our side to
do the same (or perhaps force it on him).

undo
Sent (twice) as confirmation of the 'remove' command from peer. Upon
reception the latter can update his state copy of the remote GUI by taking
back one ply. In addition he should now assume the remote GUI has dropped
out of playing mode, and will try to renegociate a game start by sending a
new 'go'.

quit
Sent as last command before closing a connection, so that the peer knows
the connection was terminated intentionally.
Are e's really that expensive?!

Matthew:out
Some believe in the almighty dollar.

I believe in the almighty printf statement.
User avatar
hgm
Posts: 27788
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

Re: Peer-to-peer GUI adapter

Post by hgm »

I am now checking how I can port this to Linux, but I get the impression that all this socket stuff is pretty much platform independent. Is it really a matter of just changing a few #includes?