Because that's a feature of the shell (of ksh, copied by bash), and the shell only.
/dev/tcp/... are not real files, the shell intercepts the attempts to redirect to a
/dev/tcp/... file and then does a
socket(...);connect(...) (makes a TCP connection) instead of a
open("/dev/tcp/..."...) (opening that file) in that case.
Note that it has to be spelled like that.
cat < /dev/./tcp/... or
///dev/tcp/... won't work, and will attempt to open those files instead (which on most systems don't exist and you'll get an error).
The direction of the redirection also doesn't matter. Whether you use
3< /dev/tcp/... or
3> /dev/tcp/... or
3<> /dev/tcp/... or even
3>> /dev/tcp/... won't make any difference, you'll be able to both read and write from/to that file descriptor to receive/send data over that TCP socket.
When you do
cat /dev/tcp/..., that doesn't work because
cat doesn't implement that same special handling, it does a
open("/dev/tcp/...") like for every file (except
-), only the shell (ksh, bash only) does, and only for the target of redirections.
cat - is another example of a file path handled specially, this time, by
cat, not the shell.
Instead of doing a
open("-") and reading the input from the resulting file descriptor,
cat reads directly from the file descriptor 0 (stdin).
cat and many text utilities do that, the shell doesn't for its redirections. To read the content of the
- file, you need
cat ./-, or
cat < - (or
cat - < -). On systems that don't a have
bash will however do something similar for redirections from that (virtual) file. GNU
awk does the same for
/dev/stderr even on systems that do have such files which can cause some surprises on systems like Linux where those files behave differently.
zsh also has TCP (and Unix domain stream) socket support, but that's done with a
zsocket) builtins, so it's less limited than the ksh/bash approach. In particular, it can also act as a server which ksh/bash can't do. It's still much more limited than what you can do in a real programming language though.