You can call the ic_websocket_cdk::ws_send function in any update call in your canister. You can also expose expose an update method to call the ws_send (e.g. from another canister using inter-canister calls). We did the same for the ic_websocket_cdk’s integration test canister. You can have a look at it here.
I think here you were just confusing the ws_message with the ws_send. The ws_message should just be used in the ws_message update method exposed by the canister.
You get the client_key for the first time when the on_open callback is called. The on_open callback that you pass to the CDK must accept an argument of type OnOpenCallbackArgs, which is a struct that contains the client_key. In the ic_websocket_example you can see it:
So, in the on_open callback you can for example save the client_key in a map and read it from that map later when in your logic you need to call the ws_send.